Índice

  1. Introducción
  2. Cargar librerías
  3. Cargar datos
  4. Visualización y preprocesamiento
    4.1 Imputación de valores nulos
    4.2 Codificación de variables categóricas
    4.3 Escalado de variables numéricas
  5. Análisis de datos
    5.1 Análisis aprendizaje supervisado
    5.2 Análisis aprendizaje no supervisado
    5.2.1 K-means
    5.2.2 Clustering jerárquico

Introduction

Los datos provienen de la página web Kaggle. El dataset contiene información sobre setas comestibles y venenosas. El dataset contiene 8124 observaciones y 24 variables. Las variables con las que trabajaremos son las siguientes:

1. class: edible=e, poisonous=p
2. cap-diameter: continuous
3. cap-shape: bell=b, conical=c, convex=x, flat=f, knobbed=k, sunken=s
4. cap-surface: fibrous=f, grooves=g, scaly=y, smooth=s
5. cap-color: brown=n, buff=b, cinnamon=c, gray=g, green=r, pink=p, purple=u, red=e, white=w, yellow=y
6. bruises: bruises=t, no=f
7. odor: almond=a, anise=l, creosote=c, fishy=y, foul=f, musty=m, none=n, pungent=p, spicy=s
8. gill-attachment: attached=a, descending=d, free=f, notched=n
9. gill-spacing: close=c, crowded=w, distant=d
10. gill-size: broad=b, narrow=n
11. gill-color: black=k, brown=n, buff=b, chocolate=h, gray=g, green=r, orange=o, pink=p, purple=u, red=e, white=w, yellow=y
12. stalk-shape: enlarging=e, tapering=t
13. stalk-root: bulbous=b, club=c, cup=u, equal=e, rhizomorphs=z, rooted=r, missing=?
14. stalk-surface-above-ring: fibrous=f, scaly=y, silky=k, smooth=s
15. stalk-surface-below-ring: fibrous=f, scaly=y, silky=k, smooth=s
16. stalk-color-above-ring: brown=n, buff=b, cinnamon=c, gray=g, orange=o, pink=p, red=e, white=w, yellow=y
17. stalk-color-below-ring: brown=n, buff=b, cinnamon=c, gray=g, orange=o, pink=p, red=e, white=w, yellow=y
18. veil-type: partial=p, universal=u
19. veil-color: brown=n, orange=o, white=w, yellow=y
20. ring-number: none=n, one=o, two=t
21. ring-type: cobwebby=c, evanescent=e, flaring=f, large=l, none=n, pendant=p, sheathing=s, zone=z
22. spore-print-color: black=k, brown=n, buff=b, chocolate=h, green=r, orange=o, purple=u, white=w, yellow=y
23. population: abundant=a, clustered=c, numerous=n, scattered=s, several=v, solitary=y
24. habitat: grasses=g, leaves=l, meadows=m, paths=p, urban=u, waste=w, woods=d

Cargar librerías

install.packages("caret")
trying URL 'https://cran.rstudio.com/bin/macosx/big-sur-arm64/contrib/4.2/caret_6.0-93.tgz'
Content type 'application/x-gzip' length 3578325 bytes (3.4 MB)
==================================================
downloaded 3.4 MB

The downloaded binary packages are in
    /var/folders/v0/47swnvh93cl0_bw1dw8tl_0h0000gn/T//RtmpBAcuFX/downloaded_packages
install.packages("tidyverse")
trying URL 'https://cran.rstudio.com/bin/macosx/big-sur-arm64/contrib/4.2/tidyverse_1.3.2.tgz'
Content type 'application/x-gzip' length 425892 bytes (415 KB)
==================================================
downloaded 415 KB

The downloaded binary packages are in
    /var/folders/v0/47swnvh93cl0_bw1dw8tl_0h0000gn/T//RtmpBAcuFX/downloaded_packages
install.packages("plotly")
trying URL 'https://cran.rstudio.com/bin/macosx/big-sur-arm64/contrib/4.2/plotly_4.10.1.tgz'
Content type 'application/x-gzip' length 3179590 bytes (3.0 MB)
==================================================
downloaded 3.0 MB

The downloaded binary packages are in
    /var/folders/v0/47swnvh93cl0_bw1dw8tl_0h0000gn/T//RtmpBAcuFX/downloaded_packages
install.packages("dplyr")
trying URL 'https://cran.rstudio.com/bin/macosx/big-sur-arm64/contrib/4.2/dplyr_1.0.10.tgz'
Content type 'application/x-gzip' length 1327807 bytes (1.3 MB)
==================================================
downloaded 1.3 MB

The downloaded binary packages are in
    /var/folders/v0/47swnvh93cl0_bw1dw8tl_0h0000gn/T//RtmpBAcuFX/downloaded_packages
install.packages("factoextra")
trying URL 'https://cran.rstudio.com/bin/macosx/big-sur-arm64/contrib/4.2/factoextra_1.0.7.tgz'
Content type 'application/x-gzip' length 415155 bytes (405 KB)
==================================================
downloaded 405 KB

The downloaded binary packages are in
    /var/folders/v0/47swnvh93cl0_bw1dw8tl_0h0000gn/T//RtmpBAcuFX/downloaded_packages
install.packages("dendextend")
trying URL 'https://cran.rstudio.com/bin/macosx/big-sur-arm64/contrib/4.2/dendextend_1.16.0.tgz'
Content type 'application/x-gzip' length 3895351 bytes (3.7 MB)
==================================================
downloaded 3.7 MB

The downloaded binary packages are in
    /var/folders/v0/47swnvh93cl0_bw1dw8tl_0h0000gn/T//RtmpBAcuFX/downloaded_packages
library(caret)
Loading required package: ggplot2
Loading required package: lattice
Registered S3 method overwritten by 'data.table':
  method           from
  print.data.table     
library(tidyverse)
Registered S3 methods overwritten by 'dbplyr':
  method         from
  print.tbl_lazy     
  print.tbl_sql      
── Attaching packages ──────────────────────────────────────────────────── tidyverse 1.3.2 ──✔ tibble  3.1.8      ✔ dplyr   1.0.10
✔ tidyr   1.2.1      ✔ stringr 1.4.1 
✔ readr   2.1.3      ✔ forcats 0.5.2 
✔ purrr   0.3.5      ── Conflicts ─────────────────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
✖ purrr::lift()   masks caret::lift()
library(plotly)
Registered S3 method overwritten by 'htmlwidgets':
  method           from         
  print.htmlwidget tools:rstudio

Attaching package: ‘plotly’

The following object is masked from ‘package:ggplot2’:

    last_plot

The following object is masked from ‘package:stats’:

    filter

The following object is masked from ‘package:graphics’:

    layout
library(dplyr)
library(cluster)
library(factoextra)
Welcome! Want to learn more? See two factoextra-related books at https://goo.gl/ve3WBa
library(dendextend)

---------------------
Welcome to dendextend version 1.16.0
Type citation('dendextend') for how to cite the package.

Type browseVignettes(package = 'dendextend') for the package vignette.
The github page is: https://github.com/talgalili/dendextend/

Suggestions and bug-reports can be submitted at: https://github.com/talgalili/dendextend/issues
You may ask questions at stackoverflow, use the r and dendextend tags: 
     https://stackoverflow.com/questions/tagged/dendextend

    To suppress this message use:  suppressPackageStartupMessages(library(dendextend))
---------------------


Attaching package: ‘dendextend’

The following object is masked from ‘package:stats’:

    cutree

Cargar datos

En primer lugar, realizamos la lectura del fichero csv, separándolo por “;” y mostramos las primeras 6 filas del fichero.

mushroom <- read.csv("./data/data.csv", sep = ";")
head(mushroom)

Echamos un vistazo a las características de los atributos del dataset. En el caso de las variables numéricas, se puede observar valores como el mínimo, máximo, media, desviación estándar, etc.

summary(mushroom)
    class            cap.diameter     cap.shape         cap.surface       
 Length:61069       Min.   : 0.380   Length:61069       Length:61069      
 Class :character   1st Qu.: 3.480   Class :character   Class :character  
 Mode  :character   Median : 5.860   Mode  :character   Mode  :character  
                    Mean   : 6.734                                        
                    3rd Qu.: 8.540                                        
                    Max.   :62.340                                        
  cap.color         does.bruise.or.bleed gill.attachment    gill.spacing      
 Length:61069       Length:61069         Length:61069       Length:61069      
 Class :character   Class :character     Class :character   Class :character  
 Mode  :character   Mode  :character     Mode  :character   Mode  :character  
                                                                              
                                                                              
                                                                              
  gill.color         stem.height       stem.width      stem.root         stem.surface      
 Length:61069       Min.   : 0.000   Min.   :  0.00   Length:61069       Length:61069      
 Class :character   1st Qu.: 4.640   1st Qu.:  5.21   Class :character   Class :character  
 Mode  :character   Median : 5.950   Median : 10.19   Mode  :character   Mode  :character  
                    Mean   : 6.582   Mean   : 12.15                                        
                    3rd Qu.: 7.740   3rd Qu.: 16.57                                        
                    Max.   :33.920   Max.   :103.91                                        
  stem.color         veil.type          veil.color          has.ring        
 Length:61069       Length:61069       Length:61069       Length:61069      
 Class :character   Class :character   Class :character   Class :character  
 Mode  :character   Mode  :character   Mode  :character   Mode  :character  
                                                                            
                                                                            
                                                                            
  ring.type         spore.print.color    habitat             season         
 Length:61069       Length:61069       Length:61069       Length:61069      
 Class :character   Class :character   Class :character   Class :character  
 Mode  :character   Mode  :character   Mode  :character   Mode  :character  
                                                                            
                                                                            
                                                                            

Visualización y preprocesamiento

Debido a que el dataset contiene valores vacíos, los sustituiremos por valores NA para poder trabajar con ellos.

mushroom[mushroom == ""] <- NA

Comprobamos que se han sustituido correctamente los valores vacíos por NA.

str(mushroom)
'data.frame':   61069 obs. of  21 variables:
 $ class               : chr  "p" "p" "p" "p" ...
 $ cap.diameter        : num  15.3 16.6 14.1 14.2 14.6 ...
 $ cap.shape           : chr  "x" "x" "x" "f" ...
 $ cap.surface         : chr  "g" "g" "g" "h" ...
 $ cap.color           : chr  "o" "o" "o" "e" ...
 $ does.bruise.or.bleed: chr  "f" "f" "f" "f" ...
 $ gill.attachment     : chr  "e" "e" "e" "e" ...
 $ gill.spacing        : chr  NA NA NA NA ...
 $ gill.color          : chr  "w" "w" "w" "w" ...
 $ stem.height         : num  16.9 18 17.8 15.8 16.5 ...
 $ stem.width          : num  17.1 18.2 17.7 16 17.2 ...
 $ stem.root           : chr  "s" "s" "s" "s" ...
 $ stem.surface        : chr  "y" "y" "y" "y" ...
 $ stem.color          : chr  "w" "w" "w" "w" ...
 $ veil.type           : chr  "u" "u" "u" "u" ...
 $ veil.color          : chr  "w" "w" "w" "w" ...
 $ has.ring            : chr  "t" "t" "t" "t" ...
 $ ring.type           : chr  "g" "g" "g" "p" ...
 $ spore.print.color   : chr  NA NA NA NA ...
 $ habitat             : chr  "d" "d" "d" "d" ...
 $ season              : chr  "w" "u" "w" "w" ...

Comprobaremos la cantidad de valores nulos que hay en cada variable. Para ello, utilizaremos la función colSums(is.na(mushroom)), que nos devolverá la suma de valores nulos de cada variable. Esta información nos ayudará a decidir si será conveniente eliminar dichas variables o no. Podemos observar que existen 5 variables donde más del 50% de los valores son nulos. Estas son: stem.surface, veil.color, spore.print.color, stem.root, veil.type. Esta información nos ayudará a decidir si será conveniente eliminar dichas variables o no. En este caso, dichas variables serán eliminadas ya que, en caso de imputar los valores nulos, se perdería mucha información.

colSums(is.na(mushroom))
               class         cap.diameter            cap.shape          cap.surface 
                   0                    0                    0                14120 
           cap.color does.bruise.or.bleed      gill.attachment         gill.spacing 
                   0                    0                 9884                25063 
          gill.color          stem.height           stem.width            stem.root 
                   0                    0                    0                51538 
        stem.surface           stem.color            veil.type           veil.color 
               38124                    0                57892                53656 
            has.ring            ring.type    spore.print.color              habitat 
                   0                 2471                54715                    0 
              season 
                   0 

En este caso, decidimos eliminar aquellas variables con más del 50% de valores nulos ya que, en caso de imputar los valores nulos, se perdería mucha información. Para ello, en primer lugar, obtendremos el nombre de las columnas que queremos eliminar.

nacols <- colnames(mushroom)[colSums(is.na(mushroom)) > nrow(mushroom) / 2]
print(nacols)
[1] "stem.root"         "stem.surface"      "veil.type"         "veil.color"       
[5] "spore.print.color"

Eliminamos las variables no deseadas.

mushroom <- mushroom[, !names(mushroom) %in% nacols]

Comprobamos que se han eliminado las variables con más del 50% de valores nulos.

print(colnames(mushroom))
 [1] "class"                "cap.diameter"         "cap.shape"           
 [4] "cap.surface"          "cap.color"            "does.bruise.or.bleed"
 [7] "gill.attachment"      "gill.spacing"         "gill.color"          
[10] "stem.height"          "stem.width"           "stem.color"          
[13] "has.ring"             "ring.type"            "habitat"             
[16] "season"              

A continuación, separamos las variables numéricas de las categóricas.

colsnames <- colnames(mushroom)
numerical_features <- c("cap.diameter", "stem.height", "stem.width")
categorical_features <- colsnames[!colsnames %in% numerical_features]
print(categorical_features)
 [1] "class"                "cap.shape"            "cap.surface"         
 [4] "cap.color"            "does.bruise.or.bleed" "gill.attachment"     
 [7] "gill.spacing"         "gill.color"           "stem.color"          
[10] "has.ring"             "ring.type"            "habitat"             
[13] "season"              
print(numerical_features)
[1] "cap.diameter" "stem.height"  "stem.width"  

Visualizamos la distribución de las variables categóricas a través de histogramas. Observaremos los posibles valores de cada variable categórica junto con su frecuencia de aparición en el dataset.

for (i in categorical_features) {
  print(ggplot(mushroom, aes_string(x = i)) +
    geom_bar(fill = "#7fd6d9") +
    geom_text(stat = "count", aes(label = scales::percent(..count.. / nrow(mushroom)), vjust = -0.25)) +
    labs(x = i, y = "Percentage") +
    theme(axis.text.x = element_text(angle = 90, hjust = 1)))
}
Warning: `aes_string()` was deprecated in ggplot2 3.0.0.
Please use tidy evaluation ideoms with `aes()`

Tras visualizar los histogramas de cada variable, llegamos a las siguientes conclusiones:

A continuación, visualizaremos la distribución de las variables numéricas a través de una gráfica generada con “featurePlot”. Esta requiere que la variable objetivo sea de tipo factor, por lo que hacemos la conversión.

mushroom$class <- as.factor(mushroom$class)
featurePlot(x = mushroom[, numerical_features], y = mushroom$class, plot = "strip")

Con la gráfica anterior, se puede observar que para valores altos de cap.diameter, stem.height y stem.width, la probabilidad de que la clase sea “e” es mayor que la de “p”.

Para decidir si es conveniente eliminar alguna variable, se puede utilizar la función “nearZeroVar”. Esta función devuelve un vector con los índices de las variables que tienen una varianza cercana a 0.

near_zero_col <- nearZeroVar(mushroom, saveMetrics = FALSE)
colnames(mushroom)[c(near_zero_col)]
[1] "ring.type"

Como conclusión, la variable “ring.type” podría ser eliminada ya que tiene una varianza cercana a 0. A continuación, visualizaremos la correlación existente con la variable dependiente “class”.

print(ggplot(mushroom, aes_string(x = "ring.type")) +
  geom_bar(aes(fill = class)))

Tras visualizar la gráfica anterior, se puede observar que la distribución de la variable dependiente “class” es similar en casi todos los valores de “ring.type”. Sin embargo, en el caso de “ring.type” = “z” y “ring.type” = “m”, la distribución de la variable dependiente “class” es diferente. Por lo tanto, se decide no eliminar la variable “ring.type”.

Imputación de valores nulos

Como se ha comentado anteriormente, los valores nulos de las variables categóricas se imputarán a través de la moda de cada variable. Esto es debido a que la función preProcess() de la librería caret no permite imputar valores nulos de variables categóricas.

for (i in categorical_features) {
  mushroom[, i][is.na(mushroom[, i])] <- names(which.max(table(mushroom[, i])))
}

Podemos comprobar que ya no existen valores nulos en las variables categóricas.

colSums(is.na(mushroom))
               class         cap.diameter            cap.shape          cap.surface 
                   0                    0                    0                    0 
           cap.color does.bruise.or.bleed      gill.attachment         gill.spacing 
                   0                    0                    0                    0 
          gill.color          stem.height           stem.width           stem.color 
                   0                    0                    0                    0 
            has.ring            ring.type              habitat               season 
                   0                    0                    0                    0 

Escalado de variables numéricas

Para escalar las variables numéricas, se utilizará la función preProcess() de la librería caret. Esta función devuelve un objeto de tipo “preProcess” que contiene la información necesaria para escalar las variables numéricas. A continuación, se escalarán las variables numéricas y se eliminarán las variables originales.

range_numeric <- preProcess(mushroom[, numerical_features], method = c("range"))
mushroom[, numerical_features] <- predict(range_numeric, newdata = mushroom[, numerical_features])
str(mushroom)
'data.frame':   61069 obs. of  16 variables:
 $ class               : Factor w/ 2 levels "e","p": 2 2 2 2 2 2 2 2 2 2 ...
 $ cap.diameter        : num  0.24 0.262 0.221 0.223 0.23 ...
 $ cap.shape           : chr  "x" "x" "x" "f" ...
 $ cap.surface         : chr  "g" "g" "g" "h" ...
 $ cap.color           : chr  "o" "o" "o" "e" ...
 $ does.bruise.or.bleed: chr  "f" "f" "f" "f" ...
 $ gill.attachment     : chr  "e" "e" "e" "e" ...
 $ gill.spacing        : chr  "c" "c" "c" "c" ...
 $ gill.color          : chr  "w" "w" "w" "w" ...
 $ stem.height         : num  0.5 0.53 0.525 0.465 0.487 ...
 $ stem.width          : num  0.164 0.175 0.171 0.154 0.166 ...
 $ stem.color          : chr  "w" "w" "w" "w" ...
 $ has.ring            : chr  "t" "t" "t" "t" ...
 $ ring.type           : chr  "g" "g" "g" "p" ...
 $ habitat             : chr  "d" "d" "d" "d" ...
 $ season              : chr  "w" "u" "w" "w" ...

Análisis de datos

Análisis aprendizaje supervisado

Regresión Logística

Primero se dividirá el dataset en dos conjuntos: uno de entrenamiento y otro de test. El conjunto de entrenamiento se utilizará para entrenar los modelos y el conjunto de test se utilizará para evaluarlos.

library(caTools)
set.seed(18)

split <- sample.split(mushroom$class, SplitRatio = 0.8)
training_set <- subset(mushroom, split == TRUE)
test_set <- subset(mushroom, split == FALSE)

table(training_set$class)

    e     p 
21745 27110 
table(test_set$class)

   e    p 
5436 6778 

(Redactar) La regresión logística permite predecir el resultado de una variable categórica en función de las variables independientes o predictoras.

rl_classiffier <- glm(class ~ ., family = binomial, data = training_set)
summary(rl_classiffier)

Call:
glm(formula = class ~ ., family = binomial, data = training_set)

Deviance Residuals: 
     Min        1Q    Median        3Q       Max  
-2.98113  -0.77757   0.00045   0.75561   2.99676  

Coefficients: (2 not defined because of singularities)
                        Estimate Std. Error z value Pr(>|z|)    
(Intercept)           -16.146622 310.494270  -0.052 0.958526    
cap.diameter           -3.097299   0.273141 -11.340  < 2e-16 ***
cap.shapec             -1.587320   0.089853 -17.666  < 2e-16 ***
cap.shapef             -1.548574   0.056840 -27.244  < 2e-16 ***
cap.shapeo             -0.505473   0.102060  -4.953 7.32e-07 ***
cap.shapep             -1.355581   0.077525 -17.486  < 2e-16 ***
cap.shapes             -1.914206   0.067428 -28.389  < 2e-16 ***
cap.shapex             -1.587070   0.052730 -30.098  < 2e-16 ***
cap.surfacee            0.777609   0.081079   9.591  < 2e-16 ***
cap.surfaceg           -0.469069   0.067815  -6.917 4.62e-12 ***
cap.surfaceh           -0.869361   0.063758 -13.635  < 2e-16 ***
cap.surfacei            1.878605   0.116592  16.113  < 2e-16 ***
cap.surfacek            3.205155   0.106739  30.028  < 2e-16 ***
cap.surfacel           -1.338110   0.100947 -13.256  < 2e-16 ***
cap.surfaces           -0.913831   0.059362 -15.394  < 2e-16 ***
cap.surfacet           -0.037125   0.050461  -0.736 0.461905    
cap.surfacew           -0.483820   0.084640  -5.716 1.09e-08 ***
cap.surfacey           -0.579375   0.064113  -9.037  < 2e-16 ***
cap.colore              1.986990   0.109951  18.072  < 2e-16 ***
cap.colorg              0.582318   0.108490   5.367 7.98e-08 ***
cap.colork              1.553011   0.131690  11.793  < 2e-16 ***
cap.colorl             -0.129820   0.140094  -0.927 0.354103    
cap.colorn              0.374666   0.099619   3.761 0.000169 ***
cap.coloro              1.627975   0.112745  14.439  < 2e-16 ***
cap.colorp              1.051060   0.127472   8.245  < 2e-16 ***
cap.colorr              2.925006   0.136340  21.454  < 2e-16 ***
cap.coloru              1.538956   0.121348  12.682  < 2e-16 ***
cap.colorw              0.861936   0.104891   8.217  < 2e-16 ***
cap.colory              0.615058   0.102936   5.975 2.30e-09 ***
does.bruise.or.bleedt  -0.147991   0.038250  -3.869 0.000109 ***
gill.attachmentd        0.644857   0.046979  13.726  < 2e-16 ***
gill.attachmente       -0.922859   0.055651 -16.583  < 2e-16 ***
gill.attachmentf        0.816396   0.136750   5.970 2.37e-09 ***
gill.attachmentp       -2.450165   0.060717 -40.354  < 2e-16 ***
gill.attachments        0.142043   0.052091   2.727 0.006395 ** 
gill.attachmentx        0.089014   0.043392   2.051 0.040231 *  
gill.spacingd          -0.425196   0.036863 -11.534  < 2e-16 ***
gill.spacingf                 NA         NA      NA       NA    
gill.colore             2.304077   0.156835  14.691  < 2e-16 ***
gill.colorf                   NA         NA      NA       NA    
gill.colorg             0.833125   0.124899   6.670 2.55e-11 ***
gill.colork             0.854961   0.136405   6.268 3.66e-10 ***
gill.colorn             1.350254   0.119303  11.318  < 2e-16 ***
gill.coloro             1.131719   0.123598   9.156  < 2e-16 ***
gill.colorp             0.848246   0.122841   6.905 5.01e-12 ***
gill.colorr             0.896493   0.144108   6.221 4.94e-10 ***
gill.coloru             1.319431   0.145603   9.062  < 2e-16 ***
gill.colorw             0.819802   0.115149   7.120 1.08e-12 ***
gill.colory             1.818426   0.119373  15.233  < 2e-16 ***
stem.height             3.580359   0.196103  18.258  < 2e-16 ***
stem.width             -0.455339   0.225084  -2.023 0.043076 *  
stem.colore            17.897727 310.494233   0.058 0.954033    
stem.colorf            35.399323 334.067154   0.106 0.915610    
stem.colorg            15.906809 310.494228   0.051 0.959142    
stem.colork            19.545176 310.494262   0.063 0.949807    
stem.colorl            15.591779 310.494286   0.050 0.959950    
stem.colorn            17.491009 310.494224   0.056 0.955077    
stem.coloro            16.096825 310.494231   0.052 0.958654    
stem.colorp            19.131727 310.494251   0.062 0.950868    
stem.colorr            17.537012 310.494253   0.056 0.954959    
stem.coloru            17.419638 310.494223   0.056 0.955260    
stem.colorw            16.082601 310.494224   0.052 0.958691    
stem.colory            17.437089 310.494226   0.056 0.955215    
has.ringt              -0.006793   0.050561  -0.134 0.893124    
ring.typef             -0.911592   0.084553 -10.781  < 2e-16 ***
ring.typeg             -0.499679   0.101534  -4.921 8.60e-07 ***
ring.typel             -0.167323   0.100958  -1.657 0.097449 .  
ring.typem            -20.187930 230.159347  -0.088 0.930105    
ring.typep              0.894990   0.103503   8.647  < 2e-16 ***
ring.typer             -0.492646   0.105898  -4.652 3.29e-06 ***
ring.typez             16.741684  78.753199   0.213 0.831651    
habitatg                0.548824   0.041040  13.373  < 2e-16 ***
habitath                0.199731   0.065306   3.058 0.002225 ** 
habitatl               -0.501569   0.054900  -9.136  < 2e-16 ***
habitatm                0.103008   0.067655   1.523 0.127870    
habitatp               17.002090 230.326776   0.074 0.941156    
habitatu              -17.054326 378.536784  -0.045 0.964065    
habitatw              -17.436003 215.700665  -0.081 0.935574    
seasons                -1.618439   0.069225 -23.379  < 2e-16 ***
seasonu                 0.181688   0.025335   7.171 7.42e-13 ***
seasonw                -1.277528   0.049110 -26.014  < 2e-16 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 67137  on 48854  degrees of freedom
Residual deviance: 45160  on 48776  degrees of freedom
AIC: 45318

Number of Fisher Scoring iterations: 16

Una vez hemos aplicado la función glm(), obtenemos los valores residuales del modelo y los coeficientes de ajuste para cada una de las variables independientes. Además, obtenemos el p-value correspondiente a cada una de ellas. Las variables con dos o tres asteriscos aportan bastante relevancia como predictores; sin embargo, las variables que no poseen ninguno o uno son de menor relevancia.

A continuación, procedemos a predecir las clases del conjunto de entrenamiento y de validación. Tomamos como umbral 0,5, de forma que si la probabilidad queda por encima de dicho umbral es que es comestible, pero si queda por debajo es que es venenoso. Por último crearemos la matriz de confusión para valorar los resultados de las predicciones.

pred_train <- predict(rl_classiffier, newdata = training_set, type = "response")
Warning: prediction from a rank-deficient fit may be misleading
pred_train <- ifelse(pred_train > 0.5, "p", "e")
pred_train <- factor(pred_train, levels = c("e", "p"), labels = c("e", "p"))

confusion_m <- table(training_set$class, pred_train)
confusion_m
   pred_train
        e     p
  e 16489  5256
  p  5573 21537
accuracy <- sum(diag(confusion_m)) / sum(confusion_m)
accuracy
[1] 0.7783441

Una vez hemos visto el resultado que muestra la matriz de confusión, se observa que la predicción tiene una precisión del 77,8%, lo que puede resultar algo baja.

Repetimos el mismo proceso para el conjunto de test.

pred_test <- predict(rl_classiffier, newdata = test_set, type = "response")
Warning: prediction from a rank-deficient fit may be misleading
pred_test <- ifelse(pred_test > 0.5, "p", "e")
pred_test <- factor(pred_test, levels = c("e", "p"), labels = c("e", "p"))

confusion_m <- table(test_set$class, pred_test)
confusion_m
   pred_test
       e    p
  e 4087 1349
  p 1420 5358
accuracy_rl <- sum(diag(confusion_m)) / sum(confusion_m)
accuracy_rl
[1] 0.7732929

Obtenemos una precisión con gran similitud a la del conjunto de training, un 77,3%. Para finalizar la regresión logística, vamos a graficar la curva ROC, la cual nos aporta la visualización de la relación entre los falsos y verdaderos positivos.

library(ROCR)
pred_rl_roc <- prediction(as.numeric(pred_test), as.numeric(test_set$class))
perf_rl_roc <- performance(pred_rl_roc, "tpr", "fpr")
perf_rl_auc <- performance(pred_rl_roc, "auc")

print(perf_rl_auc@y.values[[1]])
[1] 0.7711691
plot(perf_rl_roc)

Observando la curva ROC resultante podemos comentar que se mantiene por encima de la diagonal, lo que es buena señal, pero se aproxima a ella, pudiendo haber proporcionado resultados mejores.

k-NN

Antes de nada, para poder aplicar knn, debemos transformar las variables categóricas en numéricas.

mushroom_num <- dummyVars(" ~ .", data = mushroom, fullRank = TRUE) %>% predict(mushroom)
mushroom_num <- as.data.frame(mushroom_num)

Visualizamos las variables categóricas codificadas y las dimensiones del dataset.

dim_mushroom <- dim(mushroom_num)
print(dim_mushroom)
[1] 61069    81
str(mushroom_num)
'data.frame':   61069 obs. of  81 variables:
 $ class.p              : num  1 1 1 1 1 1 1 1 1 1 ...
 $ cap.diameter         : num  0.24 0.262 0.221 0.223 0.23 ...
 $ cap.shapec           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.shapef           : num  0 0 0 1 0 0 1 0 1 1 ...
 $ cap.shapeo           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.shapep           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.shapes           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.shapex           : num  1 1 1 0 1 1 0 1 0 0 ...
 $ cap.surfacee         : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.surfaceg         : num  1 1 1 0 0 1 0 0 1 1 ...
 $ cap.surfaceh         : num  0 0 0 1 1 0 1 1 0 0 ...
 $ cap.surfacei         : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.surfacek         : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.surfacel         : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.surfaces         : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.surfacet         : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.surfacew         : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.surfacey         : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.colore           : num  0 0 0 1 0 0 0 1 0 1 ...
 $ cap.colorg           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.colork           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.colorl           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.colorn           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.coloro           : num  1 1 1 0 1 1 1 0 1 0 ...
 $ cap.colorp           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.colorr           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.coloru           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.colorw           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ cap.colory           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ does.bruise.or.bleedt: num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.attachmentd     : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.attachmente     : num  1 1 1 1 1 1 1 1 1 1 ...
 $ gill.attachmentf     : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.attachmentp     : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.attachments     : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.attachmentx     : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.spacingd        : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.spacingf        : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.colore          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.colorf          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.colorg          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.colork          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.colorn          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.coloro          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.colorp          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.colorr          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.coloru          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ gill.colorw          : num  1 1 1 1 1 1 1 1 1 1 ...
 $ gill.colory          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ stem.height          : num  0.5 0.53 0.525 0.465 0.487 ...
 $ stem.width           : num  0.164 0.175 0.171 0.154 0.166 ...
 $ stem.colore          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ stem.colorf          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ stem.colorg          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ stem.colork          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ stem.colorl          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ stem.colorn          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ stem.coloro          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ stem.colorp          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ stem.colorr          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ stem.coloru          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ stem.colorw          : num  1 1 1 1 1 1 1 1 1 1 ...
 $ stem.colory          : num  0 0 0 0 0 0 0 0 0 0 ...
 $ has.ringt            : num  1 1 1 1 1 1 1 1 1 1 ...
 $ ring.typef           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ ring.typeg           : num  1 1 1 0 0 0 1 0 0 0 ...
 $ ring.typel           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ ring.typem           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ ring.typep           : num  0 0 0 1 1 1 0 1 1 1 ...
 $ ring.typer           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ ring.typez           : num  0 0 0 0 0 0 0 0 0 0 ...
 $ habitatg             : num  0 0 0 0 0 0 0 0 0 0 ...
 $ habitath             : num  0 0 0 0 0 0 0 0 0 0 ...
 $ habitatl             : num  0 0 0 0 0 0 0 0 0 0 ...
 $ habitatm             : num  0 0 0 0 0 0 0 0 0 0 ...
 $ habitatp             : num  0 0 0 0 0 0 0 0 0 0 ...
 $ habitatu             : num  0 0 0 0 0 0 0 0 0 0 ...
 $ habitatw             : num  0 0 0 0 0 0 0 0 0 0 ...
 $ seasons              : num  0 0 0 0 0 0 0 0 0 0 ...
 $ seasonu              : num  0 1 0 0 0 1 0 1 0 0 ...
 $ seasonw              : num  1 0 1 1 1 0 1 0 0 1 ...
library(caTools)
set.seed(18)

split <- sample.split(mushroom_num$class, SplitRatio = 0.8)
training_set_num <- subset(mushroom_num, split == TRUE)
test_set_num <- subset(mushroom_num, split == FALSE)

table(training_set_num$class)

    0     1 
21745 27110 
table(test_set_num$class)

   0    1 
5436 6778 

Para comenzar a aplicar el clasificador del vecino más cercano, haremos uso de la función knn() tomando como valor de k=5.

library(class)
set.seed(18)
pred_knn <- knn(train = training_set_num[, -1], test = test_set_num[, -1], cl = training_set_num$class, k = 5)

Una vez tenemos los resultados de las predicciones, cosntruimos la matriz de confusión.

summary(pred_knn)
   0    1 
5429 6785 
confusionM <- table(test_set_num$class, pred_knn)
confusionM
   pred_knn
       0    1
  0 5429    7
  1    0 6778
accuracy_knn <- sum(diag(confusionM)) / sum(confusionM)
accuracy_knn
[1] 0.9994269

Como resultado de la matriz de confusión, tenemos una precisión del 99%, resultando ser mejor que en la regresión logística.

library(ROCR)
pred_knn_roc <- prediction(as.numeric(pred_knn), as.numeric(test_set_num$class))
perf_knn_roc <- performance(pred_knn_roc, "tpr", "fpr")
perf_knn_auc <- performance(pred_knn_roc, "auc")

print(perf_knn_auc@y.values[[1]])
[1] 0.9993561
plot(perf_knn_roc)

Una vez obtenemos la curva ROC y el resultado del área de debajo de la misma, observamos que el resultado es muy positivo, ya que gráficamente se aproxima a la vertical.

Clasificación con Árbol de Decisión

Comenzamos aplicando el árbol de decisión mediante la función rpart() como se muestra a continuación.

library(rpart)

Attaching package: ‘rpart’

The following object is masked from ‘package:dendextend’:

    prune
set.seed(18)
dt_classiffier <- rpart(class ~ ., data = training_set)
library(rattle)
Loading required package: bitops
Rattle: A free graphical interface for data science with R.
Version 5.5.1 Copyright (c) 2006-2021 Togaware Pty Ltd.
Type 'rattle()' to shake, rattle, and roll your data.
tree2 <- rpart(class ~ ., training_set,
  method = "class",
  control = rpart.control(cp = 0.00001)
)
fancyRpartPlot(tree2)


pruned <- prune(tree2, cp = 0.01)
fancyRpartPlot(pruned)

Construimos la matriz de confusión al igual que en los casos anteriores.

pred_dt <- predict(dt_classiffier, newdata = test_set, type = "class")

confusionM <- table(test_set$class, pred_dt)
confusionM
   pred_dt
       e    p
  e 4257 1179
  p  804 5974
accuracy_dt <- sum(diag(confusionM)) / sum(confusionM)
accuracy_dt
[1] 0.8376453

Una vez tenemos el resultado para el valor de precisión en la predicción una vez aplicado el árbol de decisión, 83%, pasamos a aplicar de forma visual la construcción de la gráfica de la curva ROC y el cálculo del AUC. En este caso, continúa siendo mejor resultado que el obtenido mediante la regresión logística, pero quedando por debajo que el resultado obtenido al aplicar KNN.

library(ROCR)
pred_dt_roc <- prediction(as.numeric(pred_dt), as.numeric(test_set$class))
perf_dt_roc <- performance(pred_dt_roc, "tpr", "fpr")
perf_dt_auc <- performance(pred_dt_roc, "auc")

print(perf_dt_auc@y.values[[1]])
[1] 0.8322468
plot(perf_dt_roc)

Random Forests classiffier

Comenzamos aplicando el clasificador de Random Forest mediante la función llamada de la misma forma; es decir, randomForest(). En esta función, el valor correspondiente al parámetro ntree indica la cantidad de árboles de decisión que formarán parte del clasificador.

library(randomForest)
randomForest 4.7-1.1
Type rfNews() to see new features/changes/bug fixes.

Attaching package: ‘randomForest’

The following object is masked from ‘package:rattle’:

    importance

The following object is masked from ‘package:dplyr’:

    combine

The following object is masked from ‘package:ggplot2’:

    margin
set.seed(18)
rf_classiffier <- randomForest(class ~ ., data = training_set, ntree = 250)

Realizamos las predicciones sobre el conjunto de datos y construimos la matriz de confusión.

pred_rf <- predict(rf_classiffier, newdata = test_set, type = "class")

confusionM <- table(test_set$class, pred_rf)
confusionM
   pred_rf
       e    p
  e 5431    5
  p    0 6778
accuracy_rf <- sum(diag(confusionM)) / sum(confusionM)
accuracy_rf
[1] 0.9995906

De forma similar al clasificador KNN, obtenemos un valor de precisión del 99%. Definimos la curva ROC para este caso y realizamos el cálculo del área bajo la curva, los cuales nos simbolizan su altísima precisión.

library(ROCR)
pred_rf_roc <- prediction(as.numeric(pred_rf), as.numeric(test_set$class))
perf_rf_roc <- performance(pred_rf_roc, "tpr", "fpr")
perf_rf_auc <- performance(pred_rf_roc, "auc")

print(perf_rf_auc@y.values[[1]])
[1] 0.9995401
plot(perf_rf_roc)

Kernel SVM Classifier

Comenzamos a aplicar el clasificador Máquina de Soporte Vectorial haciendo uso de la función svm(). En esta función, los valores de los parámetros type y kernel hacen referencia al tipo de clasificador; es decir, que el kernel es de tipo radial y gaussiano.

library(e1071)
set.seed(18)
svm_classiffier <- svm(class ~ .,
  data = training_set,
  type = "C-classification", kernel = "radial"
)

La predicción y la matríz de confusión son entonces las mostradas a continuación.

pred_svm <- predict(svm_classiffier, newdata = test_set, type = "class")

confusionM <- table(test_set$class, pred_svm)
confusionM
   pred_svm
       e    p
  e 5129  307
  p  240 6538
accuracy_svm <- sum(diag(confusionM)) / sum(confusionM)
accuracy_svm
[1] 0.9552153

Como se puede observar el valor de la predicción en este caso resulta ser del 95%. Para finalizar, construimos la curva ROC correspondiente en este caso

library(ROCR)
pred_svm_roc <- prediction(as.numeric(pred_svm), as.numeric(test_set$class))
perf_svm_roc <- performance(pred_svm_roc, "tpr", "fpr")
perf_svm_auc <- performance(pred_svm_roc, "auc")

print(perf_svm_auc@y.values[[1]])
[1] 0.954058
plot(perf_svm_roc)

Conclusiones

Una vez aplicados los cinco distintos métodos de clasificación sobre nuestro dataset tras realizar el preprocesamiento, podemos concluir diciendo que los clasificadores de KNN y Random Forest son los que poseen mayor precisión y, por tanto, menor error de predicción. Sin embargo, con los resultados obtenidos por parte de ambos podemos detectar un posible sobreajuste, no beneficiando al modelo. Por esta razón, creemos que resulta más beneficioso sacrificar parte del valor de precisión, teniendo en cuenta algunos valores de falsos positivos y falsos negativos, como ocurre en el caso del clasificador de la Máquina de Soporte Vectorial, aprovechando así su capacidad de mayor generalización.

accuracy_comp <- matrix(c(accuracy_rl, accuracy_knn, accuracy_dt, accuracy_rf, accuracy_svm), ncol = 5)
barplot(accuracy_comp)


barplot(accuracy_comp,
  main = "Accuracy Comparison",
  xlab = "Accuracy (%)",
  ylab = "Method",
  names.arg = c("RL", "K-NN", "DT", "RF", "SVM"),
  col = "darkred"
)

perf_auc <- matrix(c(perf_rl_auc@y.values[[1]], perf_knn_auc@y.values[[1]], perf_dt_auc@y.values[[1]], perf_rf_auc@y.values[[1]], perf_svm_auc@y.values[[1]]), ncol = 5)
barplot(perf_auc)


barplot(perf_auc,
  main = "AUC Comparison",
  xlab = "AUC (%)",
  ylab = "Method",
  names.arg = c("RL", "K-NN", "DT", "RF", "SVM"),
  col = "darkred"
)

Análisis aprendizaje no supervisado

En este apartado se analizará el dataset a través de algoritmos de aprendizaje no supervisado. En concreto, se probarán los algoritmos k-means y clustering jerárquico. Para ambos algoritmos, se seguirá el siguiente esquema:

  1. Representar la distribución inicial de los datos.
  2. Determinar el número de clusters óptimo.
  3. Representar la distribución de los datos en función del número de clusters.
  4. Calcular el promedio de las variables en función del cluster al que pertenecen.
  5. Calcular el accuracy del algoritmo.

Para poder trabajar con algoritmos no supervisados será necesario que las variables sean numéricas. Para ello, se eliminarán las variables categóricas del dataset.

numerical_columns <- mushroom[, numerical_features]

K-means

En primer lugar, representaremos de forma gráfica la distribución inicial de los datos.

mushroom <- dummyVars(" ~ .", data = mushroom, fullRank = TRUE) %>% predict(mushroom)
mushroom <- as.data.frame(mushroom)
df <- as.data.frame(numerical_columns)

plot_ly(df,
  x = ~cap.diameter, y = ~stem.height,
  z = ~stem.width
) %>%
  add_markers(size = 1.5)

Para aplicar el algoritmo kmeans se utilizará la función kmeans() de la librería cluster. Será necesario determinar el número de clusters óptimo. Para ello, se utilizará la función “kmeans” con diferentes valores de “centers” y se calculará la suma de cuadrados internos (within groups sum of squares) para cada valor de “centers”. A continuación, se representará la suma de cuadrados internos vs. número de clusters.

wss_per_k <- 0
for (i in 1:10) {
  kmeans_aux <- kmeans(numerical_columns, center = i, nstar = 20)
  wss_per_k[i] <- kmeans_aux$tot.withinss
}
Warning: Quick-TRANSfer stage steps exceeded maximum (= 3053450)Warning: Quick-TRANSfer stage steps exceeded maximum (= 3053450)Warning: Quick-TRANSfer stage steps exceeded maximum (= 3053450)Warning: Quick-TRANSfer stage steps exceeded maximum (= 3053450)Warning: Quick-TRANSfer stage steps exceeded maximum (= 3053450)Warning: Quick-TRANSfer stage steps exceeded maximum (= 3053450)Warning: Quick-TRANSfer stage steps exceeded maximum (= 3053450)Warning: Quick-TRANSfer stage steps exceeded maximum (= 3053450)Warning: Quick-TRANSfer stage steps exceeded maximum (= 3053450)Warning: Quick-TRANSfer stage steps exceeded maximum (= 3053450)Warning: Quick-TRANSfer stage steps exceeded maximum (= 3053450)Warning: Quick-TRANSfer stage steps exceeded maximum (= 3053450)
par(mfrow = c(1, 1))
plot(1:10, wss_per_k,
  type = "b",
  xlab = "Number of clusters",
  ylab = "WSS",
)

Como se puede observar en la gráfica anterior, la suma de cuadrados internos disminuye a medida que aumenta el número de clusters. Sin embargo, a partir de 2 clusters, la disminución de la suma de cuadrados internos es muy pequeña. Por lo tanto, se decide utilizar 2 clusters. Comprobamos que tiene sentido utilizar 2 clusters, ya que conocemos que el dataset es binario.

Generamos el modelo de k-means con 2 clusters.

km_model <- kmeans(df, center = 2, nstar = 20)

Representamos la distribución de los datos en función de los clusters obtenidos.

df$cluster <- factor(km_model$cluster)

plot_ly(df,
  x = ~cap.diameter, y = ~stem.height,
  z = ~stem.width, color = ~cluster
) %>%
  add_markers(size = 1.5)
Warning: minimal value for n is 3, returning requested palette with 3 different levels
Warning: minimal value for n is 3, returning requested palette with 3 different levels
Warning: minimal value for n is 3, returning requested palette with 3 different levels
Warning: minimal value for n is 3, returning requested palette with 3 different levels

Se puede observar que los champiñones de menor tamaño (incluyendo diámetro, altura y anchura) pertenecen al cluster 2 y los de mayor tamaño pertenecen al cluster 1.

A continuación, calcularemos el valor promedio de las variables para cada cluster generado con el modelo de k-means.

grouped_mushroom <- df %>%
  group_by(cluster) %>%
  summarise(
    mean_cap_diameter = mean(cap.diameter),
    mean_stem_height = mean(stem.height),
    mean_stem_width = mean(stem.width)
  )

grouped_mushroom

A partir de este momento, hemos decidido modificar el dataset actual debido a que para aplicar técnicas como “silhouette” o “dendrogram” (para el caso de clustering jerárquico) es necesario que el dataset sea de menor tamaño. Para mantener la proporción de los datos de cada clase, se reducirá el dataset haciendo uso de createDataPartition, manteniendo únicamente un 1% de los datos iniciales para el análisis.

set.seed(42)
split <- createDataPartition(mushroom$class, p = 0.01)
smaller_df <- mushroom[split$Resample1, ]

Comprobamos que la proporción de datos de cada clase se mantiene al hacer la partición.

initial_class_prop <- table(mushroom$class) / nrow(mushroom)
smaller_class_prop <- table(smaller_df$class) / nrow(smaller_df)

print(initial_class_prop)

        0         1 
0.4450867 0.5549133 
print(smaller_class_prop)

        0         1 
0.4386252 0.5613748 

Ya podemos trabajar con el dataset reducido. Comprobamos que el dataset ahora cuenta con 611 ejemplos y 3 atributos.

smaller_df <- smaller_df[, numerical_features]
dim(smaller_df)
[1] 611   3

En primer lugar, visualizaremos la distribución inicial de los datos a través de una gráfica 3D.

plot_ly(smaller_df,
  x = ~cap.diameter, y = ~stem.height,
  z = ~stem.width
) %>%
  add_markers(size = 1.5)

A continuación, vamos a estudiar cuál sería el número óptimo de clusters para el dataset reducido haciendo uso de la medida de bondad interna “silhouette”. Para ello, utilizaremos la función fviz_nbclust de factoextra. Silhouette es una medida que sirve para validar el número de clusters. Se calcula como la diferencia entre la distancia media de un punto a los puntos de su propio cluster y la distancia media de un punto a los puntos de su cluster más cercano.

fviz_nbclust(smaller_df, FUNcluster = kmeans, method = "silhouette")

Según la gráfica, podemos afirmar que el número óptimo de clusters es 2.

Por último, visualizamos la distribución de los datos en función de los clusters obtenidos.

km_sm_model <- kmeans(smaller_df, center = 2, nstart = 20)
cluster <- factor(km_sm_model$cluster)

plot_ly(smaller_df,
  x = ~cap.diameter, y = ~stem.height,
  z = ~stem.width, color = ~cluster
) %>%
  add_markers(size = 1.5)
Warning: minimal value for n is 3, returning requested palette with 3 different levels
Warning: minimal value for n is 3, returning requested palette with 3 different levels
Warning: minimal value for n is 3, returning requested palette with 3 different levels
Warning: minimal value for n is 3, returning requested palette with 3 different levels

Por último, calculamos el valor promedio de las variables para cada cluster generado con el modelo de k-means.

grouped_sm_mushroom <- smaller_df %>%
  mutate(cluster = cluster) %>%
  group_by(cluster) %>%
  summarise(
    mean_cap_diameter = mean(cap.diameter),
    mean_stem_height = mean(stem.height),
    mean_stem_width = mean(stem.width)
  )
grouped_sm_mushroom

Clustering jerárquico

A continuación, vamos a aplicar el algoritmo de clustering jerárquico a nuestro dataset reducido. Para ello, utilizaremos la función hclust. Primero, calculamos la distancia entre los puntos del dataset.

distance <- dist(smaller_df)
hc_model <- hclust(distance)

Representamos el dendrograma para visualizar la distribución de los datos en función de los clusters obtenidos.

dend_modelo <- as.dendrogram(hc_model)
plot(dend_modelo)

Hasta ahora, hemos obtenido la jerarquía de los datos, pero lo que realmente nos interesa es la clasificación de los datos en función de los clusters. Cortaremos el dendrograma en un punto que nos interese para obtener los clusters. En este caso, hemos decidido cortar el dendrograma en 90 para obtener una visualización del dendograma cortado.

cut <- 0.9

dend_modelo %>%
  color_branches(h = cut) %>%
  color_labels(h = cut) %>%
  plot()

Para obtener el número óptimo de cluster, haremos uso de la medida interna de bondad silhouette. Para ello, utilizaremos la función fviz_nbclust de factoextra.

fviz_nbclust(smaller_df, FUNcluster = hcut, method = "silhouette")

Comprobamos que en este caso, el número óptimo de clusters podría ser 2 o 3, ya que el valor de silhouette es muy similar para ambos casos. En este caso, hemos decidido utilizar 2 clusters para poder comparar posteriormente los resultados con los obtenidos con el algoritmo de k-means.

Calculamos la agrupación del modelo en función del número de clusters que hemos decidido utilizar. Además, calculamos el promedio de los datos de cada cluster para ver si podemos sacar alguna conclusión.

jq_cluster <- cutree(hc_model, k = 2)

grouped_mushroom <- smaller_df %>%
  mutate(cluster = jq_cluster) %>%
  group_by(cluster) %>%
  summarise_all(mean)
grouped_mushroom

Visualizamos la agrupación de los datos en función de los clusters obtenidos.

jq_cluster <- factor(jq_cluster)

plot_ly(smaller_df,
  x = ~cap.diameter, y = ~stem.height,
  z = ~stem.width,
  color = ~jq_cluster
) %>%
  add_markers(size = 1.5)
Warning: minimal value for n is 3, returning requested palette with 3 different levels
Warning: minimal value for n is 3, returning requested palette with 3 different levels
Warning: minimal value for n is 3, returning requested palette with 3 different levels
Warning: minimal value for n is 3, returning requested palette with 3 different levels

Con el objetivo de comparar los resultados obtenidos en los dos algoritmos, vamos a calcular el rendimiento de cada uno de ellos, haciendo uso del accuracy como medida de bondad externa.

En primer lugar, calculamos el accuracy del modelo de k-means. Supondremos que la clase 1 es la clase “e” y la clase 2 es la clase “p”. Para ello, obtenemos las clases reales y las clases predichas, y calculamos el accuracy.

Volvemos a obtener el dataset reducido para poder tener las clases reales.

smaller_df <- mushroom[split$Resample1, ]
real_classes <- ifelse(smaller_df$class == "e", 1, 2)
predicted_classes <- km_sm_model$cluster
predicted_classes <- as.numeric(predicted_classes)
accuracy <- sum(real_classes == predicted_classes) / length(real_classes)
print(accuracy)
[1] 0.700491

Hacemos lo mismo con el modelo de clustering jerárquico, pero en este caso, supondremos que la clase 1 es la clase “p” y la clase 2 es la clase “e”.

real_classes <- ifelse(smaller_df$class == "e", 2, 1)
predicted_classes <- as.numeric(jq_cluster)
accuracy <- sum(real_classes == predicted_classes) / length(real_classes)
print(accuracy)
[1] 0.9885434

Tras comparar los resultados obtenidos en los dos algoritmos, podemos afirmar que el modelo de clustering jerárquico ha obtenido un accuracy mayor para este dataset.

LS0tCnRpdGxlOiAiTXVzaHJvb20gRGF0YSBBbmFseXNpcyIKb3V0cHV0OgogIGh0bWxfZG9jdW1lbnQ6CiAgICBkZl9wcmludDogcGFnZWQKICBwZGZfZG9jdW1lbnQ6IGRlZmF1bHQKICBodG1sX25vdGVib29rOiBkZWZhdWx0Ci0tLQoKIyDDjW5kaWNlICAgCjEuIFtJbnRyb2R1Y2Npw7NuXSgjaW50cm9kdWN0aW9uKSBcCjIuIFtDYXJnYXIgbGlicmVyw61hc10oI2xpYnJhcmllcykgXAozLiBbQ2FyZ2FyIGRhdG9zXSgjZGF0YSkgXAo0LiBbVmlzdWFsaXphY2nDs24geSBwcmVwcm9jZXNhbWllbnRvXSgjcHJlcHJvY2Vzc2luZykgXAo0LjEgW0ltcHV0YWNpw7NuIGRlIHZhbG9yZXMgbnVsb3NdKCNpbXB1dGF0aW9uKSBcCjQuMiBbQ29kaWZpY2FjacOzbiBkZSB2YXJpYWJsZXMgY2F0ZWfDs3JpY2FzXSgjZW5jb2RpbmcpIFwKNC4zIFtFc2NhbGFkbyBkZSB2YXJpYWJsZXMgbnVtw6lyaWNhc10oI3NjYWxpbmcpCjUuIFtBbsOhbGlzaXMgZGUgZGF0b3NdKCNhbmFseXNpcykgXAo1LjEgW0Fuw6FsaXNpcyBhcHJlbmRpemFqZSBzdXBlcnZpc2Fkb10oI3N1cGVydmlzZWQpIFwKNS4yIFtBbsOhbGlzaXMgYXByZW5kaXphamUgbm8gc3VwZXJ2aXNhZG9dKCN1bnN1cGVydmlzZWQpIFwKNS4yLjEgW0stbWVhbnNdKCNrbWVhbnMpIFwKNS4yLjIgW0NsdXN0ZXJpbmcgamVyw6FycXVpY29dKCNoaWVyYXJjaGljYWwpIFwKClxwYWdlYnJlYWsKCiMgSW50cm9kdWN0aW9uPGEgbmFtZT0iaW50cm9kdWN0aW9uIj48L2E+CgpMb3MgZGF0b3MgcHJvdmllbmVuIGRlIGxhIHDDoWdpbmEgd2ViIEthZ2dsZS4gRWwgZGF0YXNldCBjb250aWVuZSBpbmZvcm1hY2nDs24gc29icmUgc2V0YXMgY29tZXN0aWJsZXMgeSB2ZW5lbm9zYXMuIEVsIGRhdGFzZXQgY29udGllbmUgODEyNCBvYnNlcnZhY2lvbmVzIHkgMjQgdmFyaWFibGVzLgpMYXMgdmFyaWFibGVzIGNvbiBsYXMgcXVlIHRyYWJhamFyZW1vcyBzb24gbGFzIHNpZ3VpZW50ZXM6CgoqKjEuIGNsYXNzOioqIGVkaWJsZT1lLCBwb2lzb25vdXM9cCBcCioqMi4gY2FwLWRpYW1ldGVyOioqIGNvbnRpbnVvdXMgXAoqKjMuIGNhcC1zaGFwZToqKiBiZWxsPWIsIGNvbmljYWw9YywgY29udmV4PXgsIGZsYXQ9Ziwga25vYmJlZD1rLCBzdW5rZW49cyBcCioqNC4gY2FwLXN1cmZhY2U6KiogZmlicm91cz1mLCBncm9vdmVzPWcsIHNjYWx5PXksIHNtb290aD1zIFwKKio1LiBjYXAtY29sb3I6KiogYnJvd249biwgYnVmZj1iLCBjaW5uYW1vbj1jLCBncmF5PWcsIGdyZWVuPXIsIHBpbms9cCwgcHVycGxlPXUsIHJlZD1lLCB3aGl0ZT13LCB5ZWxsb3c9eSBcCioqNi4gYnJ1aXNlczoqKiBicnVpc2VzPXQsIG5vPWYgXAoqKjcuIG9kb3I6KiogYWxtb25kPWEsIGFuaXNlPWwsIGNyZW9zb3RlPWMsIGZpc2h5PXksIGZvdWw9ZiwgbXVzdHk9bSwgbm9uZT1uLCBwdW5nZW50PXAsIHNwaWN5PXMgXAoqKjguIGdpbGwtYXR0YWNobWVudDoqKiBhdHRhY2hlZD1hLCBkZXNjZW5kaW5nPWQsIGZyZWU9Ziwgbm90Y2hlZD1uIFwKKio5LiBnaWxsLXNwYWNpbmc6KiogY2xvc2U9YywgY3Jvd2RlZD13LCBkaXN0YW50PWQgXAoqKjEwLiBnaWxsLXNpemU6KiogYnJvYWQ9YiwgbmFycm93PW4gXAoqKjExLiBnaWxsLWNvbG9yOioqIGJsYWNrPWssIGJyb3duPW4sIGJ1ZmY9YiwgY2hvY29sYXRlPWgsIGdyYXk9ZywgZ3JlZW49ciwgb3JhbmdlPW8sIHBpbms9cCwgcHVycGxlPXUsIHJlZD1lLCB3aGl0ZT13LCB5ZWxsb3c9eSBcCioqMTIuIHN0YWxrLXNoYXBlOioqIGVubGFyZ2luZz1lLCB0YXBlcmluZz10IFwKKioxMy4gc3RhbGstcm9vdDoqKiBidWxib3VzPWIsIGNsdWI9YywgY3VwPXUsIGVxdWFsPWUsIHJoaXpvbW9ycGhzPXosIHJvb3RlZD1yLCBtaXNzaW5nPT8gXAoqKjE0LiBzdGFsay1zdXJmYWNlLWFib3ZlLXJpbmc6KiogZmlicm91cz1mLCBzY2FseT15LCBzaWxreT1rLCBzbW9vdGg9cyBcCioqMTUuIHN0YWxrLXN1cmZhY2UtYmVsb3ctcmluZzoqKiBmaWJyb3VzPWYsIHNjYWx5PXksIHNpbGt5PWssIHNtb290aD1zIFwKKioxNi4gc3RhbGstY29sb3ItYWJvdmUtcmluZzoqKiBicm93bj1uLCBidWZmPWIsIGNpbm5hbW9uPWMsIGdyYXk9Zywgb3JhbmdlPW8sIHBpbms9cCwgcmVkPWUsIHdoaXRlPXcsIHllbGxvdz15IFwKKioxNy4gc3RhbGstY29sb3ItYmVsb3ctcmluZzoqKiBicm93bj1uLCBidWZmPWIsIGNpbm5hbW9uPWMsIGdyYXk9Zywgb3JhbmdlPW8sIHBpbms9cCwgcmVkPWUsIHdoaXRlPXcsIHllbGxvdz15IFwKKioxOC4gdmVpbC10eXBlOioqIHBhcnRpYWw9cCwgdW5pdmVyc2FsPXUgXAoqKjE5LiB2ZWlsLWNvbG9yOioqIGJyb3duPW4sIG9yYW5nZT1vLCB3aGl0ZT13LCB5ZWxsb3c9eSBcCioqMjAuIHJpbmctbnVtYmVyOioqIG5vbmU9biwgb25lPW8sIHR3bz10IFwKKioyMS4gcmluZy10eXBlOioqIGNvYndlYmJ5PWMsIGV2YW5lc2NlbnQ9ZSwgZmxhcmluZz1mLCBsYXJnZT1sLCBub25lPW4sIHBlbmRhbnQ9cCwgc2hlYXRoaW5nPXMsIHpvbmU9eiBcCioqMjIuIHNwb3JlLXByaW50LWNvbG9yOioqIGJsYWNrPWssIGJyb3duPW4sIGJ1ZmY9YiwgY2hvY29sYXRlPWgsIGdyZWVuPXIsIG9yYW5nZT1vLCBwdXJwbGU9dSwgd2hpdGU9dywgeWVsbG93PXkgXAoqKjIzLiBwb3B1bGF0aW9uOioqIGFidW5kYW50PWEsIGNsdXN0ZXJlZD1jLCBudW1lcm91cz1uLCBzY2F0dGVyZWQ9cywgc2V2ZXJhbD12LCBzb2xpdGFyeT15IFwKKioyNC4gaGFiaXRhdDoqKiBncmFzc2VzPWcsIGxlYXZlcz1sLCBtZWFkb3dzPW0sIHBhdGhzPXAsIHVyYmFuPXUsIHdhc3RlPXcsIHdvb2RzPWQgXAoKIyBDYXJnYXIgbGlicmVyw61hczxhIG5hbWU9ImxpYnJhcmllcyI+PC9hPgoKYGBge3J9Cmluc3RhbGwucGFja2FnZXMoImNhcmV0IikKaW5zdGFsbC5wYWNrYWdlcygidGlkeXZlcnNlIikKaW5zdGFsbC5wYWNrYWdlcygicGxvdGx5IikKaW5zdGFsbC5wYWNrYWdlcygiZHBseXIiKQppbnN0YWxsLnBhY2thZ2VzKCJmYWN0b2V4dHJhIikKaW5zdGFsbC5wYWNrYWdlcygiZGVuZGV4dGVuZCIpCmBgYApgYGB7cn0KbGlicmFyeShjYXJldCkKbGlicmFyeSh0aWR5dmVyc2UpCmxpYnJhcnkocGxvdGx5KQpsaWJyYXJ5KGRwbHlyKQpsaWJyYXJ5KGNsdXN0ZXIpCmxpYnJhcnkoZmFjdG9leHRyYSkKbGlicmFyeShkZW5kZXh0ZW5kKQpgYGAKCiMgQ2FyZ2FyIGRhdG9zPGEgbmFtZT0iZGF0YSI+PC9hPgoKRW4gcHJpbWVyIGx1Z2FyLCByZWFsaXphbW9zIGxhIGxlY3R1cmEgZGVsIGZpY2hlcm8gY3N2LCBzZXBhcsOhbmRvbG8gcG9yICI7IiB5IG1vc3RyYW1vcyBsYXMgcHJpbWVyYXMgNiBmaWxhcyBkZWwgZmljaGVyby4KYGBge3J9Cm11c2hyb29tIDwtIHJlYWQuY3N2KCIuL2RhdGEvZGF0YS5jc3YiLCBzZXAgPSAiOyIpCmhlYWQobXVzaHJvb20pCmBgYAoKRWNoYW1vcyB1biB2aXN0YXpvIGEgbGFzIGNhcmFjdGVyw61zdGljYXMgZGUgbG9zIGF0cmlidXRvcyBkZWwgZGF0YXNldC4gRW4gZWwgY2FzbyBkZSBsYXMgdmFyaWFibGVzIG51bcOpcmljYXMsIHNlIHB1ZWRlIG9ic2VydmFyIHZhbG9yZXMgY29tbyBlbCBtw61uaW1vLCBtw6F4aW1vLCBtZWRpYSwgZGVzdmlhY2nDs24gZXN0w6FuZGFyLCBldGMuICAKYGBge3J9CnN1bW1hcnkobXVzaHJvb20pCmBgYAoKIyBWaXN1YWxpemFjacOzbiB5IHByZXByb2Nlc2FtaWVudG88YSBuYW1lPSJwcmVwcm9jZXNzaW5nIj48L2E+CgpEZWJpZG8gYSBxdWUgZWwgZGF0YXNldCBjb250aWVuZSB2YWxvcmVzIHZhY8Otb3MsIGxvcyBzdXN0aXR1aXJlbW9zIHBvciB2YWxvcmVzIE5BIHBhcmEgcG9kZXIgdHJhYmFqYXIgY29uIGVsbG9zLgpgYGB7cn0KbXVzaHJvb21bbXVzaHJvb20gPT0gIiJdIDwtIE5BCmBgYAoKQ29tcHJvYmFtb3MgcXVlIHNlIGhhbiBzdXN0aXR1aWRvIGNvcnJlY3RhbWVudGUgbG9zIHZhbG9yZXMgdmFjw61vcyBwb3IgTkEuCmBgYHtyfQpzdHIobXVzaHJvb20pCmBgYAoKQ29tcHJvYmFyZW1vcyBsYSBjYW50aWRhZCBkZSB2YWxvcmVzIG51bG9zIHF1ZSBoYXkgZW4gY2FkYSB2YXJpYWJsZS4gUGFyYSBlbGxvLCB1dGlsaXphcmVtb3MgbGEgZnVuY2nDs24gY29sU3Vtcyhpcy5uYShtdXNocm9vbSkpLCBxdWUgbm9zIGRldm9sdmVyw6EgbGEgc3VtYSBkZSB2YWxvcmVzIG51bG9zIGRlIGNhZGEgdmFyaWFibGUuCkVzdGEgaW5mb3JtYWNpw7NuIG5vcyBheXVkYXLDoSBhIGRlY2lkaXIgc2kgc2Vyw6EgY29udmVuaWVudGUgZWxpbWluYXIgZGljaGFzIHZhcmlhYmxlcyBvIG5vLgpQb2RlbW9zIG9ic2VydmFyIHF1ZSBleGlzdGVuIDUgdmFyaWFibGVzIGRvbmRlIG3DoXMgZGVsIDUwJSBkZSBsb3MgdmFsb3JlcyBzb24gbnVsb3MuIEVzdGFzIHNvbjogc3RlbS5zdXJmYWNlLCB2ZWlsLmNvbG9yLCBzcG9yZS5wcmludC5jb2xvciwgc3RlbS5yb290LCB2ZWlsLnR5cGUuCkVzdGEgaW5mb3JtYWNpw7NuIG5vcyBheXVkYXLDoSBhIGRlY2lkaXIgc2kgc2Vyw6EgY29udmVuaWVudGUgZWxpbWluYXIgZGljaGFzIHZhcmlhYmxlcyBvIG5vLiBFbiBlc3RlIGNhc28sIGRpY2hhcyB2YXJpYWJsZXMgc2Vyw6FuIGVsaW1pbmFkYXMgeWEgcXVlLCBlbiBjYXNvIGRlIGltcHV0YXIgbG9zIHZhbG9yZXMgbnVsb3MsIHNlIHBlcmRlcsOtYSBtdWNoYSBpbmZvcm1hY2nDs24uCgpgYGB7cn0KY29sU3Vtcyhpcy5uYShtdXNocm9vbSkpCmBgYAoKRW4gZXN0ZSBjYXNvLCBkZWNpZGltb3MgZWxpbWluYXIgYXF1ZWxsYXMgdmFyaWFibGVzIGNvbiBtw6FzIGRlbCA1MCUgZGUgdmFsb3JlcyBudWxvcyB5YSBxdWUsIGVuIGNhc28gZGUgaW1wdXRhciBsb3MgdmFsb3JlcyBudWxvcywgc2UgcGVyZGVyw61hIG11Y2hhIGluZm9ybWFjacOzbi4KUGFyYSBlbGxvLCBlbiBwcmltZXIgbHVnYXIsIG9idGVuZHJlbW9zIGVsIG5vbWJyZSBkZSBsYXMgY29sdW1uYXMgcXVlIHF1ZXJlbW9zIGVsaW1pbmFyLgoKYGBge3J9Cm5hY29scyA8LSBjb2xuYW1lcyhtdXNocm9vbSlbY29sU3Vtcyhpcy5uYShtdXNocm9vbSkpID4gbnJvdyhtdXNocm9vbSkgLyAyXQpwcmludChuYWNvbHMpCmBgYAoKRWxpbWluYW1vcyBsYXMgdmFyaWFibGVzIG5vIGRlc2VhZGFzLgpgYGB7cn0KbXVzaHJvb20gPC0gbXVzaHJvb21bLCAhbmFtZXMobXVzaHJvb20pICVpbiUgbmFjb2xzXQpgYGAKCkNvbXByb2JhbW9zIHF1ZSBzZSBoYW4gZWxpbWluYWRvIGxhcyB2YXJpYWJsZXMgY29uIG3DoXMgZGVsIDUwJSBkZSB2YWxvcmVzIG51bG9zLgpgYGB7cn0KcHJpbnQoY29sbmFtZXMobXVzaHJvb20pKQpgYGAKCkEgY29udGludWFjacOzbiwgc2VwYXJhbW9zIGxhcyB2YXJpYWJsZXMgbnVtw6lyaWNhcyBkZSBsYXMgY2F0ZWfDs3JpY2FzLgpgYGB7cn0KY29sc25hbWVzIDwtIGNvbG5hbWVzKG11c2hyb29tKQpudW1lcmljYWxfZmVhdHVyZXMgPC0gYygiY2FwLmRpYW1ldGVyIiwgInN0ZW0uaGVpZ2h0IiwgInN0ZW0ud2lkdGgiKQpjYXRlZ29yaWNhbF9mZWF0dXJlcyA8LSBjb2xzbmFtZXNbIWNvbHNuYW1lcyAlaW4lIG51bWVyaWNhbF9mZWF0dXJlc10KcHJpbnQoY2F0ZWdvcmljYWxfZmVhdHVyZXMpCnByaW50KG51bWVyaWNhbF9mZWF0dXJlcykKYGBgCgpWaXN1YWxpemFtb3MgbGEgZGlzdHJpYnVjacOzbiBkZSBsYXMgdmFyaWFibGVzIGNhdGVnw7NyaWNhcyBhIHRyYXbDqXMgZGUgaGlzdG9ncmFtYXMuIE9ic2VydmFyZW1vcyBsb3MgcG9zaWJsZXMgdmFsb3JlcyBkZSBjYWRhIHZhcmlhYmxlIGNhdGVnw7NyaWNhIGp1bnRvIGNvbiBzdSBmcmVjdWVuY2lhIGRlIGFwYXJpY2nDs24gZW4gZWwgZGF0YXNldC4KCmBgYHtyfQpmb3IgKGkgaW4gY2F0ZWdvcmljYWxfZmVhdHVyZXMpIHsKICBwcmludChnZ3Bsb3QobXVzaHJvb20sIGFlc19zdHJpbmcoeCA9IGkpKSArCiAgICBnZW9tX2JhcihmaWxsID0gIiM3ZmQ2ZDkiKSArCiAgICBnZW9tX3RleHQoc3RhdCA9ICJjb3VudCIsIGFlcyhsYWJlbCA9IHNjYWxlczo6cGVyY2VudCguLmNvdW50Li4gLyBucm93KG11c2hyb29tKSksIHZqdXN0ID0gLTAuMjUpKSArCiAgICBsYWJzKHggPSBpLCB5ID0gIlBlcmNlbnRhZ2UiKSArCiAgICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9IDkwLCBoanVzdCA9IDEpKSkKfQpgYGAKClRyYXMgdmlzdWFsaXphciBsb3MgaGlzdG9ncmFtYXMgZGUgY2FkYSB2YXJpYWJsZSwgbGxlZ2Ftb3MgYSBsYXMgc2lndWllbnRlcyBjb25jbHVzaW9uZXM6CgoqIExhIHZhcmlhYmxlIGRlcGVuZGllbnRlICJjbGFzcyIgZXN0w6EgYmFsYW5jZWFkYSwgZXMgZGVjaXIsIGxhIGZyZWN1ZW5jaWEgZGUgYXBhcmljacOzbiBkZSAiZSIgKGVkaWJsZSkgeSAicCIgKHBvaXNvbm91cykgZXMgc2ltaWxhciwgZW4gdW5hIHByb3BvcmNpw7NuIGRlIDQ0LDUlIHkgNTUsNSUgcmVzcGVjdGl2YW1lbnRlLiBQb3IgbG8gdGFudG8sIG5vIGVzIG5lY2VzYXJpbyByZWFsaXphciB1biBiYWxhbmNlbyBkZSBsYSB2YXJpYWJsZSBkZXBlbmRpZW50ZSwgcGVybWl0aWVuZG8gYXBsaWNhciBsYSBtZWRpZGEgZGUgZXZhbHVhY2nDs24gImFjY3VyYWN5IiBwYXJhIGV2YWx1YXIgZWwgbW9kZWxvLgoqIExvcyB2YWxvcmVzIE5BIGRlIGxvcyBhdHJpYnV0b3MgcmVzdGFudGVzIHNlcsOhbiBpbXB1dGFkb3MgY29uIGxhIG1vZGEgZGUgY2FkYSB2YXJpYWJsZS4KCkEgY29udGludWFjacOzbiwgdmlzdWFsaXphcmVtb3MgbGEgZGlzdHJpYnVjacOzbiBkZSBsYXMgdmFyaWFibGVzIG51bcOpcmljYXMgYSB0cmF2w6lzIGRlIHVuYSBncsOhZmljYSBnZW5lcmFkYSBjb24gImZlYXR1cmVQbG90Ii4gRXN0YSByZXF1aWVyZSBxdWUgbGEgdmFyaWFibGUgb2JqZXRpdm8gc2VhIGRlIHRpcG8gZmFjdG9yLCBwb3IgbG8gcXVlIGhhY2Vtb3MgbGEgY29udmVyc2nDs24uCmBgYHtyfQptdXNocm9vbSRjbGFzcyA8LSBhcy5mYWN0b3IobXVzaHJvb20kY2xhc3MpCmZlYXR1cmVQbG90KHggPSBtdXNocm9vbVssIG51bWVyaWNhbF9mZWF0dXJlc10sIHkgPSBtdXNocm9vbSRjbGFzcywgcGxvdCA9ICJzdHJpcCIpCmBgYAoKQ29uIGxhIGdyw6FmaWNhIGFudGVyaW9yLCBzZSBwdWVkZSBvYnNlcnZhciBxdWUgcGFyYSB2YWxvcmVzIGFsdG9zIGRlIGNhcC5kaWFtZXRlciwgc3RlbS5oZWlnaHQgeSBzdGVtLndpZHRoLCBsYSBwcm9iYWJpbGlkYWQgZGUgcXVlIGxhIGNsYXNlIHNlYSAiZSIgZXMgbWF5b3IgcXVlIGxhIGRlICJwIi4KClBhcmEgZGVjaWRpciBzaSBlcyBjb252ZW5pZW50ZSBlbGltaW5hciBhbGd1bmEgdmFyaWFibGUsIHNlIHB1ZWRlIHV0aWxpemFyIGxhIGZ1bmNpw7NuICJuZWFyWmVyb1ZhciIuIEVzdGEgZnVuY2nDs24gZGV2dWVsdmUgdW4gdmVjdG9yIGNvbiBsb3Mgw61uZGljZXMgZGUgbGFzIHZhcmlhYmxlcyBxdWUgdGllbmVuIHVuYSB2YXJpYW56YSBjZXJjYW5hIGEgMC4KCmBgYHtyfQpuZWFyX3plcm9fY29sIDwtIG5lYXJaZXJvVmFyKG11c2hyb29tLCBzYXZlTWV0cmljcyA9IEZBTFNFKQpjb2xuYW1lcyhtdXNocm9vbSlbYyhuZWFyX3plcm9fY29sKV0KYGBgCgpDb21vIGNvbmNsdXNpw7NuLCBsYSB2YXJpYWJsZSAicmluZy50eXBlIiBwb2Ryw61hIHNlciBlbGltaW5hZGEgeWEgcXVlIHRpZW5lIHVuYSB2YXJpYW56YSBjZXJjYW5hIGEgMC4gQSBjb250aW51YWNpw7NuLCB2aXN1YWxpemFyZW1vcyBsYSBjb3JyZWxhY2nDs24gZXhpc3RlbnRlIGNvbiBsYSB2YXJpYWJsZSBkZXBlbmRpZW50ZSAiY2xhc3MiLgpgYGB7cn0KcHJpbnQoZ2dwbG90KG11c2hyb29tLCBhZXNfc3RyaW5nKHggPSAicmluZy50eXBlIikpICsKICBnZW9tX2JhcihhZXMoZmlsbCA9IGNsYXNzKSkpCmBgYAoKVHJhcyB2aXN1YWxpemFyIGxhIGdyw6FmaWNhIGFudGVyaW9yLCBzZSBwdWVkZSBvYnNlcnZhciBxdWUgbGEgZGlzdHJpYnVjacOzbiBkZSBsYSB2YXJpYWJsZSBkZXBlbmRpZW50ZSAiY2xhc3MiIGVzIHNpbWlsYXIgZW4gY2FzaSB0b2RvcyBsb3MgdmFsb3JlcyBkZSAicmluZy50eXBlIi4gU2luIGVtYmFyZ28sIGVuIGVsIGNhc28gZGUgInJpbmcudHlwZSIgPSAieiIgeSAicmluZy50eXBlIiA9ICJtIiwgbGEgZGlzdHJpYnVjacOzbiBkZSBsYSB2YXJpYWJsZSBkZXBlbmRpZW50ZSAiY2xhc3MiIGVzIGRpZmVyZW50ZS4gUG9yIGxvIHRhbnRvLCBzZSBkZWNpZGUgbm8gZWxpbWluYXIgbGEgdmFyaWFibGUgInJpbmcudHlwZSIuCgojIyBJbXB1dGFjacOzbiBkZSB2YWxvcmVzIG51bG9zPGEgbmFtZT0iaW1wdXRhdGlvbiI+PC9hPgoKQ29tbyBzZSBoYSBjb21lbnRhZG8gYW50ZXJpb3JtZW50ZSwgbG9zIHZhbG9yZXMgbnVsb3MgZGUgbGFzIHZhcmlhYmxlcyBjYXRlZ8OzcmljYXMgc2UgaW1wdXRhcsOhbiBhIHRyYXbDqXMgZGUgbGEgbW9kYSBkZSBjYWRhIHZhcmlhYmxlLiBFc3RvIGVzIGRlYmlkbyBhIHF1ZSBsYSBmdW5jacOzbiBwcmVQcm9jZXNzKCkgZGUgbGEgbGlicmVyw61hIGNhcmV0IG5vIHBlcm1pdGUgaW1wdXRhciB2YWxvcmVzIG51bG9zIGRlIHZhcmlhYmxlcyBjYXRlZ8OzcmljYXMuCgpgYGB7cn0KZm9yIChpIGluIGNhdGVnb3JpY2FsX2ZlYXR1cmVzKSB7CiAgbXVzaHJvb21bLCBpXVtpcy5uYShtdXNocm9vbVssIGldKV0gPC0gbmFtZXMod2hpY2gubWF4KHRhYmxlKG11c2hyb29tWywgaV0pKSkKfQpgYGAKClBvZGVtb3MgY29tcHJvYmFyIHF1ZSB5YSBubyBleGlzdGVuIHZhbG9yZXMgbnVsb3MgZW4gbGFzIHZhcmlhYmxlcyBjYXRlZ8OzcmljYXMuCmBgYHtyfQpjb2xTdW1zKGlzLm5hKG11c2hyb29tKSkKYGBgCgojIyBFc2NhbGFkbyBkZSB2YXJpYWJsZXMgbnVtw6lyaWNhczxhIG5hbWU9InNjYWxpbmciPjwvYT4KClBhcmEgZXNjYWxhciBsYXMgdmFyaWFibGVzIG51bcOpcmljYXMsIHNlIHV0aWxpemFyw6EgbGEgZnVuY2nDs24gcHJlUHJvY2VzcygpIGRlIGxhIGxpYnJlcsOtYSBjYXJldC4gRXN0YSBmdW5jacOzbiBkZXZ1ZWx2ZSB1biBvYmpldG8gZGUgdGlwbyAicHJlUHJvY2VzcyIgcXVlIGNvbnRpZW5lIGxhIGluZm9ybWFjacOzbiBuZWNlc2FyaWEgcGFyYSBlc2NhbGFyIGxhcyB2YXJpYWJsZXMgbnVtw6lyaWNhcy4gQSBjb250aW51YWNpw7NuLCBzZSBlc2NhbGFyw6FuIGxhcyB2YXJpYWJsZXMgbnVtw6lyaWNhcyB5IHNlIGVsaW1pbmFyw6FuIGxhcyB2YXJpYWJsZXMgb3JpZ2luYWxlcy4KCmBgYHtyfQpyYW5nZV9udW1lcmljIDwtIHByZVByb2Nlc3MobXVzaHJvb21bLCBudW1lcmljYWxfZmVhdHVyZXNdLCBtZXRob2QgPSBjKCJyYW5nZSIpKQptdXNocm9vbVssIG51bWVyaWNhbF9mZWF0dXJlc10gPC0gcHJlZGljdChyYW5nZV9udW1lcmljLCBuZXdkYXRhID0gbXVzaHJvb21bLCBudW1lcmljYWxfZmVhdHVyZXNdKQpzdHIobXVzaHJvb20pCmBgYAoKIyBBbsOhbGlzaXMgZGUgZGF0b3M8YSBuYW1lPSJhbmFseXNpcyI+PC9hPgoKIyMgQW7DoWxpc2lzIGFwcmVuZGl6YWplIHN1cGVydmlzYWRvPGEgbmFtZT0ic3VwZXJ2aXNlZCI+PC9hPgoKIyMjIFJlZ3Jlc2nDs24gTG9nw61zdGljYTxhIG5hbWU9ImxvZ2lzdGljIj48L2E+CgpQcmltZXJvIHNlIGRpdmlkaXLDoSBlbCBkYXRhc2V0IGVuIGRvcyBjb25qdW50b3M6IHVubyBkZSBlbnRyZW5hbWllbnRvIHkgb3RybyBkZSB0ZXN0LiBFbCBjb25qdW50byBkZSBlbnRyZW5hbWllbnRvIHNlIHV0aWxpemFyw6EgcGFyYSBlbnRyZW5hciBsb3MgbW9kZWxvcyB5IGVsIGNvbmp1bnRvIGRlIHRlc3Qgc2UgdXRpbGl6YXLDoSBwYXJhIGV2YWx1YXJsb3MuCgpgYGB7cn0KbGlicmFyeShjYVRvb2xzKQpzZXQuc2VlZCgxOCkKCnNwbGl0IDwtIHNhbXBsZS5zcGxpdChtdXNocm9vbSRjbGFzcywgU3BsaXRSYXRpbyA9IDAuOCkKdHJhaW5pbmdfc2V0IDwtIHN1YnNldChtdXNocm9vbSwgc3BsaXQgPT0gVFJVRSkKdGVzdF9zZXQgPC0gc3Vic2V0KG11c2hyb29tLCBzcGxpdCA9PSBGQUxTRSkKCnRhYmxlKHRyYWluaW5nX3NldCRjbGFzcykKdGFibGUodGVzdF9zZXQkY2xhc3MpCmBgYAoKKFJlZGFjdGFyKSBMYSByZWdyZXNpw7NuIGxvZ8Otc3RpY2EgcGVybWl0ZSBwcmVkZWNpciBlbCByZXN1bHRhZG8gZGUgdW5hIHZhcmlhYmxlIGNhdGVnw7NyaWNhIGVuIGZ1bmNpw7NuIGRlIGxhcyB2YXJpYWJsZXMgaW5kZXBlbmRpZW50ZXMgbyBwcmVkaWN0b3Jhcy4KCmBgYHtyfQpybF9jbGFzc2lmZmllciA8LSBnbG0oY2xhc3MgfiAuLCBmYW1pbHkgPSBiaW5vbWlhbCwgZGF0YSA9IHRyYWluaW5nX3NldCkKc3VtbWFyeShybF9jbGFzc2lmZmllcikKYGBgClVuYSB2ZXogaGVtb3MgYXBsaWNhZG8gbGEgZnVuY2nDs24gZ2xtKCksIG9idGVuZW1vcyBsb3MgdmFsb3JlcyByZXNpZHVhbGVzIGRlbCBtb2RlbG8geSBsb3MgY29lZmljaWVudGVzIGRlIGFqdXN0ZSBwYXJhIGNhZGEgdW5hIGRlIGxhcyB2YXJpYWJsZXMgaW5kZXBlbmRpZW50ZXMuCkFkZW3DoXMsIG9idGVuZW1vcyBlbCBwLXZhbHVlIGNvcnJlc3BvbmRpZW50ZSBhIGNhZGEgdW5hIGRlIGVsbGFzLiBMYXMgdmFyaWFibGVzIGNvbiBkb3MgbyB0cmVzIGFzdGVyaXNjb3MgYXBvcnRhbiBiYXN0YW50ZSByZWxldmFuY2lhIGNvbW8gcHJlZGljdG9yZXM7CnNpbiBlbWJhcmdvLCBsYXMgdmFyaWFibGVzIHF1ZSBubyBwb3NlZW4gbmluZ3VubyBvIHVubyBzb24gZGUgbWVub3IgcmVsZXZhbmNpYS4KCkEgY29udGludWFjacOzbiwgcHJvY2VkZW1vcyBhIHByZWRlY2lyIGxhcyBjbGFzZXMgZGVsIGNvbmp1bnRvIGRlIGVudHJlbmFtaWVudG8geSBkZSB2YWxpZGFjacOzbi4gVG9tYW1vcyBjb21vIHVtYnJhbCAwLDUsIGRlIGZvcm1hIHF1ZQpzaSBsYSBwcm9iYWJpbGlkYWQgcXVlZGEgcG9yIGVuY2ltYSBkZSBkaWNobyB1bWJyYWwgZXMgcXVlIGVzIGNvbWVzdGlibGUsIHBlcm8gc2kgcXVlZGEgcG9yIGRlYmFqbyBlcyBxdWUgZXMgdmVuZW5vc28uClBvciDDumx0aW1vIGNyZWFyZW1vcyBsYSBtYXRyaXogZGUgY29uZnVzacOzbiBwYXJhIHZhbG9yYXIgbG9zIHJlc3VsdGFkb3MgZGUgbGFzIHByZWRpY2Npb25lcy4KCmBgYHtyfQpwcmVkX3RyYWluIDwtIHByZWRpY3QocmxfY2xhc3NpZmZpZXIsIG5ld2RhdGEgPSB0cmFpbmluZ19zZXQsIHR5cGUgPSAicmVzcG9uc2UiKQpwcmVkX3RyYWluIDwtIGlmZWxzZShwcmVkX3RyYWluID4gMC41LCAicCIsICJlIikKcHJlZF90cmFpbiA8LSBmYWN0b3IocHJlZF90cmFpbiwgbGV2ZWxzID0gYygiZSIsICJwIiksIGxhYmVscyA9IGMoImUiLCAicCIpKQoKY29uZnVzaW9uX20gPC0gdGFibGUodHJhaW5pbmdfc2V0JGNsYXNzLCBwcmVkX3RyYWluKQpjb25mdXNpb25fbQoKYWNjdXJhY3kgPC0gc3VtKGRpYWcoY29uZnVzaW9uX20pKSAvIHN1bShjb25mdXNpb25fbSkKYWNjdXJhY3kKYGBgClVuYSB2ZXogaGVtb3MgdmlzdG8gZWwgcmVzdWx0YWRvIHF1ZSBtdWVzdHJhIGxhIG1hdHJpeiBkZSBjb25mdXNpw7NuLCBzZSBvYnNlcnZhIHF1ZSBsYSBwcmVkaWNjacOzbiB0aWVuZSB1bmEgcHJlY2lzacOzbiBkZWwgNzcsOCUsIGxvIHF1ZSBwdWVkZSByZXN1bHRhciBhbGdvIGJhamEuCgpSZXBldGltb3MgZWwgbWlzbW8gcHJvY2VzbyBwYXJhIGVsIGNvbmp1bnRvIGRlIHRlc3QuCgpgYGB7cn0KcHJlZF90ZXN0IDwtIHByZWRpY3QocmxfY2xhc3NpZmZpZXIsIG5ld2RhdGEgPSB0ZXN0X3NldCwgdHlwZSA9ICJyZXNwb25zZSIpCnByZWRfdGVzdCA8LSBpZmVsc2UocHJlZF90ZXN0ID4gMC41LCAicCIsICJlIikKcHJlZF90ZXN0IDwtIGZhY3RvcihwcmVkX3Rlc3QsIGxldmVscyA9IGMoImUiLCAicCIpLCBsYWJlbHMgPSBjKCJlIiwgInAiKSkKCmNvbmZ1c2lvbl9tIDwtIHRhYmxlKHRlc3Rfc2V0JGNsYXNzLCBwcmVkX3Rlc3QpCmNvbmZ1c2lvbl9tCgphY2N1cmFjeV9ybCA8LSBzdW0oZGlhZyhjb25mdXNpb25fbSkpIC8gc3VtKGNvbmZ1c2lvbl9tKQphY2N1cmFjeV9ybApgYGAKCk9idGVuZW1vcyB1bmEgcHJlY2lzacOzbiBjb24gZ3JhbiBzaW1pbGl0dWQgYSBsYSBkZWwgY29uanVudG8gZGUgdHJhaW5pbmcsIHVuIDc3LDMlLgpQYXJhIGZpbmFsaXphciBsYSByZWdyZXNpw7NuIGxvZ8Otc3RpY2EsIHZhbW9zIGEgZ3JhZmljYXIgbGEgY3VydmEgUk9DLCBsYSBjdWFsIG5vcyBhcG9ydGEgbGEgdmlzdWFsaXphY2nDs24gZGUgbGEgcmVsYWNpw7NuIGVudHJlIGxvcyBmYWxzb3MgeSB2ZXJkYWRlcm9zIHBvc2l0aXZvcy4KYGBge3J9CmxpYnJhcnkoUk9DUikKcHJlZF9ybF9yb2MgPC0gcHJlZGljdGlvbihhcy5udW1lcmljKHByZWRfdGVzdCksIGFzLm51bWVyaWModGVzdF9zZXQkY2xhc3MpKQpwZXJmX3JsX3JvYyA8LSBwZXJmb3JtYW5jZShwcmVkX3JsX3JvYywgInRwciIsICJmcHIiKQpwZXJmX3JsX2F1YyA8LSBwZXJmb3JtYW5jZShwcmVkX3JsX3JvYywgImF1YyIpCgpwcmludChwZXJmX3JsX2F1Y0B5LnZhbHVlc1tbMV1dKQpwbG90KHBlcmZfcmxfcm9jKQpgYGAKT2JzZXJ2YW5kbyBsYSBjdXJ2YSBST0MgcmVzdWx0YW50ZSBwb2RlbW9zIGNvbWVudGFyIHF1ZSBzZSBtYW50aWVuZSBwb3IgZW5jaW1hIGRlIGxhIGRpYWdvbmFsLCBsbyBxdWUgZXMgYnVlbmEgc2XDsWFsLCBwZXJvIHNlIGFwcm94aW1hIGEgZWxsYSwgcHVkaWVuZG8gaGFiZXIKcHJvcG9yY2lvbmFkbyByZXN1bHRhZG9zIG1lam9yZXMuCgojIyMgay1OTjxhIG5hbWU9ImtubiI+PC9hPgoKQW50ZXMgZGUgbmFkYSwgcGFyYSBwb2RlciBhcGxpY2FyIGtubiwgZGViZW1vcyB0cmFuc2Zvcm1hciBsYXMgdmFyaWFibGVzIGNhdGVnw7NyaWNhcyBlbiBudW3DqXJpY2FzLgoKYGBge3J9Cm11c2hyb29tX251bSA8LSBkdW1teVZhcnMoIiB+IC4iLCBkYXRhID0gbXVzaHJvb20sIGZ1bGxSYW5rID0gVFJVRSkgJT4lIHByZWRpY3QobXVzaHJvb20pCm11c2hyb29tX251bSA8LSBhcy5kYXRhLmZyYW1lKG11c2hyb29tX251bSkKYGBgCgpWaXN1YWxpemFtb3MgbGFzIHZhcmlhYmxlcyBjYXRlZ8OzcmljYXMgY29kaWZpY2FkYXMgeSBsYXMgZGltZW5zaW9uZXMgZGVsIGRhdGFzZXQuCmBgYHtyfQpkaW1fbXVzaHJvb20gPC0gZGltKG11c2hyb29tX251bSkKcHJpbnQoZGltX211c2hyb29tKQpzdHIobXVzaHJvb21fbnVtKQpgYGAKCmBgYHtyfQpsaWJyYXJ5KGNhVG9vbHMpCnNldC5zZWVkKDE4KQoKc3BsaXQgPC0gc2FtcGxlLnNwbGl0KG11c2hyb29tX251bSRjbGFzcywgU3BsaXRSYXRpbyA9IDAuOCkKdHJhaW5pbmdfc2V0X251bSA8LSBzdWJzZXQobXVzaHJvb21fbnVtLCBzcGxpdCA9PSBUUlVFKQp0ZXN0X3NldF9udW0gPC0gc3Vic2V0KG11c2hyb29tX251bSwgc3BsaXQgPT0gRkFMU0UpCgp0YWJsZSh0cmFpbmluZ19zZXRfbnVtJGNsYXNzKQp0YWJsZSh0ZXN0X3NldF9udW0kY2xhc3MpCmBgYAoKUGFyYSBjb21lbnphciBhIGFwbGljYXIgZWwgY2xhc2lmaWNhZG9yIGRlbCB2ZWNpbm8gbcOhcyBjZXJjYW5vLCBoYXJlbW9zIHVzbyBkZSBsYSBmdW5jacOzbiBrbm4oKSB0b21hbmRvIGNvbW8gdmFsb3IgZGUgaz01LgoKYGBge3J9CmxpYnJhcnkoY2xhc3MpCnNldC5zZWVkKDE4KQpwcmVkX2tubiA8LSBrbm4odHJhaW4gPSB0cmFpbmluZ19zZXRfbnVtWywgLTFdLCB0ZXN0ID0gdGVzdF9zZXRfbnVtWywgLTFdLCBjbCA9IHRyYWluaW5nX3NldF9udW0kY2xhc3MsIGsgPSA1KQpgYGAKVW5hIHZleiB0ZW5lbW9zIGxvcyByZXN1bHRhZG9zIGRlIGxhcyBwcmVkaWNjaW9uZXMsIGNvc250cnVpbW9zIGxhIG1hdHJpeiBkZSBjb25mdXNpw7NuLgoKYGBge3J9CnN1bW1hcnkocHJlZF9rbm4pCgpjb25mdXNpb25NIDwtIHRhYmxlKHRlc3Rfc2V0X251bSRjbGFzcywgcHJlZF9rbm4pCmNvbmZ1c2lvbk0KCmFjY3VyYWN5X2tubiA8LSBzdW0oZGlhZyhjb25mdXNpb25NKSkgLyBzdW0oY29uZnVzaW9uTSkKYWNjdXJhY3lfa25uCmBgYApDb21vIHJlc3VsdGFkbyBkZSBsYSBtYXRyaXogZGUgY29uZnVzacOzbiwgdGVuZW1vcyB1bmEgcHJlY2lzacOzbiBkZWwgOTklLCByZXN1bHRhbmRvIHNlciBtZWpvciBxdWUgZW4gbGEgcmVncmVzacOzbiBsb2fDrXN0aWNhLgoKYGBge3J9CmxpYnJhcnkoUk9DUikKcHJlZF9rbm5fcm9jIDwtIHByZWRpY3Rpb24oYXMubnVtZXJpYyhwcmVkX2tubiksIGFzLm51bWVyaWModGVzdF9zZXRfbnVtJGNsYXNzKSkKcGVyZl9rbm5fcm9jIDwtIHBlcmZvcm1hbmNlKHByZWRfa25uX3JvYywgInRwciIsICJmcHIiKQpwZXJmX2tubl9hdWMgPC0gcGVyZm9ybWFuY2UocHJlZF9rbm5fcm9jLCAiYXVjIikKCnByaW50KHBlcmZfa25uX2F1Y0B5LnZhbHVlc1tbMV1dKQpwbG90KHBlcmZfa25uX3JvYykKYGBgClVuYSB2ZXogb2J0ZW5lbW9zIGxhIGN1cnZhIFJPQyB5IGVsIHJlc3VsdGFkbyBkZWwgw6FyZWEgZGUgZGViYWpvIGRlIGxhIG1pc21hLCBvYnNlcnZhbW9zIHF1ZSBlbCByZXN1bHRhZG8gZXMgbXV5IHBvc2l0aXZvLCB5YSBxdWUgZ3LDoWZpY2FtZW50ZSBzZSBhcHJveGltYSBhIGxhIHZlcnRpY2FsLgoKIyMjIENsYXNpZmljYWNpw7NuIGNvbiDDgXJib2wgZGUgRGVjaXNpw7NuPGEgbmFtZT0iYXJib2wiPjwvYT4KCkNvbWVuemFtb3MgYXBsaWNhbmRvIGVsIMOhcmJvbCBkZSBkZWNpc2nDs24gbWVkaWFudGUgbGEgZnVuY2nDs24gcnBhcnQoKSBjb21vIHNlIG11ZXN0cmEgYSBjb250aW51YWNpw7NuLgoKYGBge3J9CmxpYnJhcnkocnBhcnQpCnNldC5zZWVkKDE4KQpkdF9jbGFzc2lmZmllciA8LSBycGFydChjbGFzcyB+IC4sIGRhdGEgPSB0cmFpbmluZ19zZXQpCmBgYApgYGB7cn0KbGlicmFyeShyYXR0bGUpCnRyZWUyIDwtIHJwYXJ0KGNsYXNzIH4gLiwgdHJhaW5pbmdfc2V0LAogIG1ldGhvZCA9ICJjbGFzcyIsCiAgY29udHJvbCA9IHJwYXJ0LmNvbnRyb2woY3AgPSAwLjAwMDAxKQopCmZhbmN5UnBhcnRQbG90KHRyZWUyKQoKcHJ1bmVkIDwtIHBydW5lKHRyZWUyLCBjcCA9IDAuMDEpCmZhbmN5UnBhcnRQbG90KHBydW5lZCkKYGBgCkNvbnN0cnVpbW9zIGxhIG1hdHJpeiBkZSBjb25mdXNpw7NuIGFsIGlndWFsIHF1ZSBlbiBsb3MgY2Fzb3MgYW50ZXJpb3Jlcy4KCmBgYHtyfQpwcmVkX2R0IDwtIHByZWRpY3QoZHRfY2xhc3NpZmZpZXIsIG5ld2RhdGEgPSB0ZXN0X3NldCwgdHlwZSA9ICJjbGFzcyIpCgpjb25mdXNpb25NIDwtIHRhYmxlKHRlc3Rfc2V0JGNsYXNzLCBwcmVkX2R0KQpjb25mdXNpb25NCgphY2N1cmFjeV9kdCA8LSBzdW0oZGlhZyhjb25mdXNpb25NKSkgLyBzdW0oY29uZnVzaW9uTSkKYWNjdXJhY3lfZHQKYGBgCgpVbmEgdmV6IHRlbmVtb3MgZWwgcmVzdWx0YWRvIHBhcmEgZWwgdmFsb3IgZGUgcHJlY2lzacOzbiBlbiBsYSBwcmVkaWNjacOzbiB1bmEgdmV6IGFwbGljYWRvIGVsIMOhcmJvbCBkZSBkZWNpc2nDs24sIDgzJSwKcGFzYW1vcyBhIGFwbGljYXIgZGUgZm9ybWEgdmlzdWFsIGxhIGNvbnN0cnVjY2nDs24gZGUgbGEgZ3LDoWZpY2EgZGUgbGEgY3VydmEgUk9DIHkgZWwgY8OhbGN1bG8gZGVsIEFVQy4gRW4gZXN0ZSBjYXNvLApjb250aW7DumEgc2llbmRvIG1lam9yIHJlc3VsdGFkbyBxdWUgZWwgb2J0ZW5pZG8gbWVkaWFudGUgbGEgcmVncmVzacOzbiBsb2fDrXN0aWNhLCBwZXJvIHF1ZWRhbmRvIHBvciBkZWJham8gcXVlIGVsCnJlc3VsdGFkbyBvYnRlbmlkbyBhbCBhcGxpY2FyIEtOTi4KCmBgYHtyfQpsaWJyYXJ5KFJPQ1IpCnByZWRfZHRfcm9jIDwtIHByZWRpY3Rpb24oYXMubnVtZXJpYyhwcmVkX2R0KSwgYXMubnVtZXJpYyh0ZXN0X3NldCRjbGFzcykpCnBlcmZfZHRfcm9jIDwtIHBlcmZvcm1hbmNlKHByZWRfZHRfcm9jLCAidHByIiwgImZwciIpCnBlcmZfZHRfYXVjIDwtIHBlcmZvcm1hbmNlKHByZWRfZHRfcm9jLCAiYXVjIikKCnByaW50KHBlcmZfZHRfYXVjQHkudmFsdWVzW1sxXV0pCnBsb3QocGVyZl9kdF9yb2MpCmBgYAoKIyMjIFJhbmRvbSBGb3Jlc3RzIGNsYXNzaWZmaWVyPGEgbmFtZT0icmFuZG9tIj48L2E+CgpDb21lbnphbW9zIGFwbGljYW5kbyBlbCBjbGFzaWZpY2Fkb3IgZGUgUmFuZG9tIEZvcmVzdCBtZWRpYW50ZSBsYSBmdW5jacOzbiBsbGFtYWRhIGRlIGxhIG1pc21hIGZvcm1hOyBlcyBkZWNpciwgcmFuZG9tRm9yZXN0KCkuCkVuIGVzdGEgZnVuY2nDs24sIGVsIHZhbG9yIGNvcnJlc3BvbmRpZW50ZSBhbCBwYXLDoW1ldHJvIG50cmVlIGluZGljYSBsYSBjYW50aWRhZCBkZSDDoXJib2xlcyBkZSBkZWNpc2nDs24gcXVlIGZvcm1hcsOhbiBwYXJ0ZSBkZWwgY2xhc2lmaWNhZG9yLgoKYGBge3J9CmxpYnJhcnkocmFuZG9tRm9yZXN0KQpzZXQuc2VlZCgxOCkKcmZfY2xhc3NpZmZpZXIgPC0gcmFuZG9tRm9yZXN0KGNsYXNzIH4gLiwgZGF0YSA9IHRyYWluaW5nX3NldCwgbnRyZWUgPSAyNTApCmBgYAoKUmVhbGl6YW1vcyBsYXMgcHJlZGljY2lvbmVzIHNvYnJlIGVsIGNvbmp1bnRvIGRlIGRhdG9zIHkgY29uc3RydWltb3MgbGEgbWF0cml6IGRlIGNvbmZ1c2nDs24uCgpgYGB7cn0KcHJlZF9yZiA8LSBwcmVkaWN0KHJmX2NsYXNzaWZmaWVyLCBuZXdkYXRhID0gdGVzdF9zZXQsIHR5cGUgPSAiY2xhc3MiKQoKY29uZnVzaW9uTSA8LSB0YWJsZSh0ZXN0X3NldCRjbGFzcywgcHJlZF9yZikKY29uZnVzaW9uTQoKYWNjdXJhY3lfcmYgPC0gc3VtKGRpYWcoY29uZnVzaW9uTSkpIC8gc3VtKGNvbmZ1c2lvbk0pCmFjY3VyYWN5X3JmCmBgYAoKRGUgZm9ybWEgc2ltaWxhciBhbCBjbGFzaWZpY2Fkb3IgS05OLCBvYnRlbmVtb3MgdW4gdmFsb3IgZGUgcHJlY2lzacOzbiBkZWwgOTklLgpEZWZpbmltb3MgbGEgY3VydmEgUk9DIHBhcmEgZXN0ZSBjYXNvIHkgcmVhbGl6YW1vcyBlbCBjw6FsY3VsbyBkZWwgw6FyZWEgYmFqbyBsYSBjdXJ2YSwgbG9zIGN1YWxlcyBub3Mgc2ltYm9saXphbiBzdSBhbHTDrXNpbWEgcHJlY2lzacOzbi4KCmBgYHtyfQpsaWJyYXJ5KFJPQ1IpCnByZWRfcmZfcm9jIDwtIHByZWRpY3Rpb24oYXMubnVtZXJpYyhwcmVkX3JmKSwgYXMubnVtZXJpYyh0ZXN0X3NldCRjbGFzcykpCnBlcmZfcmZfcm9jIDwtIHBlcmZvcm1hbmNlKHByZWRfcmZfcm9jLCAidHByIiwgImZwciIpCnBlcmZfcmZfYXVjIDwtIHBlcmZvcm1hbmNlKHByZWRfcmZfcm9jLCAiYXVjIikKCnByaW50KHBlcmZfcmZfYXVjQHkudmFsdWVzW1sxXV0pCnBsb3QocGVyZl9yZl9yb2MpCmBgYAoKIyMjIEtlcm5lbCBTVk0gQ2xhc3NpZmllcgoKQ29tZW56YW1vcyBhIGFwbGljYXIgZWwgY2xhc2lmaWNhZG9yIE3DoXF1aW5hIGRlIFNvcG9ydGUgVmVjdG9yaWFsIGhhY2llbmRvIHVzbyBkZSBsYSBmdW5jacOzbiBzdm0oKS4KRW4gZXN0YSBmdW5jacOzbiwgbG9zIHZhbG9yZXMgZGUgbG9zIHBhcsOhbWV0cm9zIHR5cGUgeSBrZXJuZWwgaGFjZW4gcmVmZXJlbmNpYSBhbCB0aXBvIGRlIGNsYXNpZmljYWRvcjsgZXMgZGVjaXIsCnF1ZSBlbCBrZXJuZWwgZXMgZGUgdGlwbyByYWRpYWwgeSBnYXVzc2lhbm8uIAoKYGBge3J9CmxpYnJhcnkoZTEwNzEpCnNldC5zZWVkKDE4KQpzdm1fY2xhc3NpZmZpZXIgPC0gc3ZtKGNsYXNzIH4gLiwKICBkYXRhID0gdHJhaW5pbmdfc2V0LAogIHR5cGUgPSAiQy1jbGFzc2lmaWNhdGlvbiIsIGtlcm5lbCA9ICJyYWRpYWwiCikKYGBgCkxhIHByZWRpY2Npw7NuIHkgbGEgbWF0csOteiBkZSBjb25mdXNpw7NuIHNvbiBlbnRvbmNlcyBsYXMgbW9zdHJhZGFzIGEgY29udGludWFjacOzbi4KCmBgYHtyfQpwcmVkX3N2bSA8LSBwcmVkaWN0KHN2bV9jbGFzc2lmZmllciwgbmV3ZGF0YSA9IHRlc3Rfc2V0LCB0eXBlID0gImNsYXNzIikKCmNvbmZ1c2lvbk0gPC0gdGFibGUodGVzdF9zZXQkY2xhc3MsIHByZWRfc3ZtKQpjb25mdXNpb25NCgphY2N1cmFjeV9zdm0gPC0gc3VtKGRpYWcoY29uZnVzaW9uTSkpIC8gc3VtKGNvbmZ1c2lvbk0pCmFjY3VyYWN5X3N2bQpgYGAKCkNvbW8gc2UgcHVlZGUgb2JzZXJ2YXIgZWwgdmFsb3IgZGUgbGEgcHJlZGljY2nDs24gZW4gZXN0ZSBjYXNvIHJlc3VsdGEgc2VyIGRlbCA5NSUuClBhcmEgZmluYWxpemFyLCBjb25zdHJ1aW1vcyBsYSBjdXJ2YSBST0MgY29ycmVzcG9uZGllbnRlIGVuIGVzdGUgY2FzbyAKCmBgYHtyfQpsaWJyYXJ5KFJPQ1IpCnByZWRfc3ZtX3JvYyA8LSBwcmVkaWN0aW9uKGFzLm51bWVyaWMocHJlZF9zdm0pLCBhcy5udW1lcmljKHRlc3Rfc2V0JGNsYXNzKSkKcGVyZl9zdm1fcm9jIDwtIHBlcmZvcm1hbmNlKHByZWRfc3ZtX3JvYywgInRwciIsICJmcHIiKQpwZXJmX3N2bV9hdWMgPC0gcGVyZm9ybWFuY2UocHJlZF9zdm1fcm9jLCAiYXVjIikKCnByaW50KHBlcmZfc3ZtX2F1Y0B5LnZhbHVlc1tbMV1dKQpwbG90KHBlcmZfc3ZtX3JvYykKYGBgCgojIyMgQ29uY2x1c2lvbmVzCgpVbmEgdmV6IGFwbGljYWRvcyBsb3MgY2luY28gZGlzdGludG9zIG3DqXRvZG9zIGRlIGNsYXNpZmljYWNpw7NuIHNvYnJlIG51ZXN0cm8gZGF0YXNldCB0cmFzIHJlYWxpemFyIGVsIHByZXByb2Nlc2FtaWVudG8sIHBvZGVtb3MgY29uY2x1aXIgZGljaWVuZG8gcXVlIGxvcyBjbGFzaWZpY2Fkb3JlcyBkZSBLTk4geSBSYW5kb20gRm9yZXN0IHNvbiBsb3MgcXVlIHBvc2VlbiBtYXlvciBwcmVjaXNpw7NuCnksIHBvciB0YW50bywgbWVub3IgZXJyb3IgZGUgcHJlZGljY2nDs24uIFNpbiBlbWJhcmdvLCBjb24gbG9zIHJlc3VsdGFkb3Mgb2J0ZW5pZG9zIHBvciBwYXJ0ZSBkZSBhbWJvcyBwb2RlbW9zIGRldGVjdGFyIHVuIHBvc2libGUgc29icmVhanVzdGUsIG5vIGJlbmVmaWNpYW5kbyBhbCBtb2RlbG8uIFBvciBlc3RhIHJhesOzbiwgY3JlZW1vcyBxdWUgcmVzdWx0YSBtw6FzIGJlbmVmaWNpb3NvIHNhY3JpZmljYXIKcGFydGUgZGVsIHZhbG9yIGRlIHByZWNpc2nDs24sIHRlbmllbmRvIGVuIGN1ZW50YSBhbGd1bm9zIHZhbG9yZXMgZGUgZmFsc29zIHBvc2l0aXZvcyB5IGZhbHNvcyBuZWdhdGl2b3MsIGNvbW8gb2N1cnJlIGVuIGVsIGNhc28gZGVsIGNsYXNpZmljYWRvciBkZSBsYSBNw6FxdWluYSBkZSBTb3BvcnRlIFZlY3RvcmlhbCwgYXByb3ZlY2hhbmRvIGFzw60gc3UgY2FwYWNpZGFkIGRlIG1heW9yIGdlbmVyYWxpemFjacOzbi4KCmBgYHtyfQphY2N1cmFjeV9jb21wIDwtIG1hdHJpeChjKGFjY3VyYWN5X3JsLCBhY2N1cmFjeV9rbm4sIGFjY3VyYWN5X2R0LCBhY2N1cmFjeV9yZiwgYWNjdXJhY3lfc3ZtKSwgbmNvbCA9IDUpCmJhcnBsb3QoYWNjdXJhY3lfY29tcCkKCmJhcnBsb3QoYWNjdXJhY3lfY29tcCwKICBtYWluID0gIkFjY3VyYWN5IENvbXBhcmlzb24iLAogIHhsYWIgPSAiQWNjdXJhY3kgKCUpIiwKICB5bGFiID0gIk1ldGhvZCIsCiAgbmFtZXMuYXJnID0gYygiUkwiLCAiSy1OTiIsICJEVCIsICJSRiIsICJTVk0iKSwKICBjb2wgPSAiZGFya3JlZCIKKQpgYGAKCmBgYHtyfQpwZXJmX2F1YyA8LSBtYXRyaXgoYyhwZXJmX3JsX2F1Y0B5LnZhbHVlc1tbMV1dLCBwZXJmX2tubl9hdWNAeS52YWx1ZXNbWzFdXSwgcGVyZl9kdF9hdWNAeS52YWx1ZXNbWzFdXSwgcGVyZl9yZl9hdWNAeS52YWx1ZXNbWzFdXSwgcGVyZl9zdm1fYXVjQHkudmFsdWVzW1sxXV0pLCBuY29sID0gNSkKYmFycGxvdChwZXJmX2F1YykKCmJhcnBsb3QocGVyZl9hdWMsCiAgbWFpbiA9ICJBVUMgQ29tcGFyaXNvbiIsCiAgeGxhYiA9ICJBVUMgKCUpIiwKICB5bGFiID0gIk1ldGhvZCIsCiAgbmFtZXMuYXJnID0gYygiUkwiLCAiSy1OTiIsICJEVCIsICJSRiIsICJTVk0iKSwKICBjb2wgPSAiZGFya3JlZCIKKQpgYGAKIyMgQW7DoWxpc2lzIGFwcmVuZGl6YWplIG5vIHN1cGVydmlzYWRvPGEgbmFtZT0idW5zdXBlcnZpc2VkIj48L2E+CgpFbiBlc3RlIGFwYXJ0YWRvIHNlIGFuYWxpemFyw6EgZWwgZGF0YXNldCBhIHRyYXbDqXMgZGUgYWxnb3JpdG1vcyBkZSBhcHJlbmRpemFqZSBubyBzdXBlcnZpc2Fkby4gRW4gY29uY3JldG8sIHNlIHByb2JhcsOhbiBsb3MgYWxnb3JpdG1vcyBrLW1lYW5zIHkgY2x1c3RlcmluZyBqZXLDoXJxdWljby4KUGFyYSBhbWJvcyBhbGdvcml0bW9zLCBzZSBzZWd1aXLDoSBlbCBzaWd1aWVudGUgZXNxdWVtYToKCjEuIFJlcHJlc2VudGFyIGxhIGRpc3RyaWJ1Y2nDs24gaW5pY2lhbCBkZSBsb3MgZGF0b3MuCjIuIERldGVybWluYXIgZWwgbsO6bWVybyBkZSBjbHVzdGVycyDDs3B0aW1vLgozLiBSZXByZXNlbnRhciBsYSBkaXN0cmlidWNpw7NuIGRlIGxvcyBkYXRvcyBlbiBmdW5jacOzbiBkZWwgbsO6bWVybyBkZSBjbHVzdGVycy4KNC4gQ2FsY3VsYXIgZWwgcHJvbWVkaW8gZGUgbGFzIHZhcmlhYmxlcyBlbiBmdW5jacOzbiBkZWwgY2x1c3RlciBhbCBxdWUgcGVydGVuZWNlbi4KNS4gQ2FsY3VsYXIgZWwgYWNjdXJhY3kgZGVsIGFsZ29yaXRtby4KClBhcmEgcG9kZXIgdHJhYmFqYXIgY29uIGFsZ29yaXRtb3Mgbm8gc3VwZXJ2aXNhZG9zIHNlcsOhIG5lY2VzYXJpbyBxdWUgbGFzIHZhcmlhYmxlcyBzZWFuIG51bcOpcmljYXMuIFBhcmEgZWxsbywgc2UgZWxpbWluYXLDoW4gbGFzIHZhcmlhYmxlcyBjYXRlZ8OzcmljYXMgZGVsIGRhdGFzZXQuCmBgYHtyfQpudW1lcmljYWxfY29sdW1ucyA8LSBtdXNocm9vbVssIG51bWVyaWNhbF9mZWF0dXJlc10KYGBgCgojIyMgSy1tZWFuczxhIG5hbWU9ImttZWFucyI+PC9hPgoKRW4gcHJpbWVyIGx1Z2FyLCByZXByZXNlbnRhcmVtb3MgZGUgZm9ybWEgZ3LDoWZpY2EgbGEgZGlzdHJpYnVjacOzbiBpbmljaWFsIGRlIGxvcyBkYXRvcy4KCmBgYHtyfQptdXNocm9vbSA8LSBkdW1teVZhcnMoIiB+IC4iLCBkYXRhID0gbXVzaHJvb20sIGZ1bGxSYW5rID0gVFJVRSkgJT4lIHByZWRpY3QobXVzaHJvb20pCm11c2hyb29tIDwtIGFzLmRhdGEuZnJhbWUobXVzaHJvb20pCmBgYAoKYGBge3J9CmRmIDwtIGFzLmRhdGEuZnJhbWUobnVtZXJpY2FsX2NvbHVtbnMpCgpwbG90X2x5KGRmLAogIHggPSB+Y2FwLmRpYW1ldGVyLCB5ID0gfnN0ZW0uaGVpZ2h0LAogIHogPSB+c3RlbS53aWR0aAopICU+JQogIGFkZF9tYXJrZXJzKHNpemUgPSAxLjUpCmBgYAoKUGFyYSBhcGxpY2FyIGVsIGFsZ29yaXRtbyBrbWVhbnMgc2UgdXRpbGl6YXLDoSBsYSBmdW5jacOzbiBrbWVhbnMoKSBkZSBsYSBsaWJyZXLDrWEgY2x1c3Rlci4gU2Vyw6EgbmVjZXNhcmlvIGRldGVybWluYXIgZWwgbsO6bWVybyBkZSBjbHVzdGVycyDDs3B0aW1vLgpQYXJhIGVsbG8sIHNlIHV0aWxpemFyw6EgbGEgZnVuY2nDs24gImttZWFucyIgY29uIGRpZmVyZW50ZXMgdmFsb3JlcyBkZSAiY2VudGVycyIgeSBzZSBjYWxjdWxhcsOhIGxhIHN1bWEgZGUgY3VhZHJhZG9zIGludGVybm9zICh3aXRoaW4gZ3JvdXBzIHN1bSBvZiBzcXVhcmVzKSBwYXJhIGNhZGEgdmFsb3IgZGUgImNlbnRlcnMiLiBBIGNvbnRpbnVhY2nDs24sIHNlIHJlcHJlc2VudGFyw6EgbGEgc3VtYSBkZSBjdWFkcmFkb3MgaW50ZXJub3MgdnMuIG7Dum1lcm8gZGUgY2x1c3RlcnMuCgpgYGB7cn0Kd3NzX3Blcl9rIDwtIDAKZm9yIChpIGluIDE6MTApIHsKICBrbWVhbnNfYXV4IDwtIGttZWFucyhudW1lcmljYWxfY29sdW1ucywgY2VudGVyID0gaSwgbnN0YXIgPSAyMCkKICB3c3NfcGVyX2tbaV0gPC0ga21lYW5zX2F1eCR0b3Qud2l0aGluc3MKfQpwYXIobWZyb3cgPSBjKDEsIDEpKQpwbG90KDE6MTAsIHdzc19wZXJfaywKICB0eXBlID0gImIiLAogIHhsYWIgPSAiTnVtYmVyIG9mIGNsdXN0ZXJzIiwKICB5bGFiID0gIldTUyIsCikKYGBgCgpDb21vIHNlIHB1ZWRlIG9ic2VydmFyIGVuIGxhIGdyw6FmaWNhIGFudGVyaW9yLCBsYSBzdW1hIGRlIGN1YWRyYWRvcyBpbnRlcm5vcyBkaXNtaW51eWUgYSBtZWRpZGEgcXVlIGF1bWVudGEgZWwgbsO6bWVybyBkZSBjbHVzdGVycy4gU2luIGVtYmFyZ28sIGEgcGFydGlyIGRlIDIgY2x1c3RlcnMsIGxhIGRpc21pbnVjacOzbiBkZSBsYSBzdW1hIGRlIGN1YWRyYWRvcyBpbnRlcm5vcyBlcyBtdXkgcGVxdWXDsWEuIFBvciBsbyB0YW50bywgc2UgZGVjaWRlIHV0aWxpemFyIDIgY2x1c3RlcnMuCkNvbXByb2JhbW9zIHF1ZSB0aWVuZSBzZW50aWRvIHV0aWxpemFyIDIgY2x1c3RlcnMsIHlhIHF1ZSBjb25vY2Vtb3MgcXVlIGVsIGRhdGFzZXQgZXMgYmluYXJpby4KCkdlbmVyYW1vcyBlbCBtb2RlbG8gZGUgay1tZWFucyBjb24gMiBjbHVzdGVycy4KCmBgYHtyfQprbV9tb2RlbCA8LSBrbWVhbnMoZGYsIGNlbnRlciA9IDIsIG5zdGFyID0gMjApCmBgYAoKUmVwcmVzZW50YW1vcyBsYSBkaXN0cmlidWNpw7NuIGRlIGxvcyBkYXRvcyBlbiBmdW5jacOzbiBkZSBsb3MgY2x1c3RlcnMgb2J0ZW5pZG9zLgpgYGB7cn0KZGYkY2x1c3RlciA8LSBmYWN0b3Ioa21fbW9kZWwkY2x1c3RlcikKCnBsb3RfbHkoZGYsCiAgeCA9IH5jYXAuZGlhbWV0ZXIsIHkgPSB+c3RlbS5oZWlnaHQsCiAgeiA9IH5zdGVtLndpZHRoLCBjb2xvciA9IH5jbHVzdGVyCikgJT4lCiAgYWRkX21hcmtlcnMoc2l6ZSA9IDEuNSkKYGBgCgpTZSBwdWVkZSBvYnNlcnZhciBxdWUgbG9zIGNoYW1wacOxb25lcyBkZSBtZW5vciB0YW1hw7FvIChpbmNsdXllbmRvIGRpw6FtZXRybywgYWx0dXJhIHkgYW5jaHVyYSkgcGVydGVuZWNlbiBhbCBjbHVzdGVyIDIgeSBsb3MgZGUgbWF5b3IgdGFtYcOxbyBwZXJ0ZW5lY2VuIGFsIGNsdXN0ZXIgMS4KCkEgY29udGludWFjacOzbiwgY2FsY3VsYXJlbW9zIGVsIHZhbG9yIHByb21lZGlvIGRlIGxhcyB2YXJpYWJsZXMgcGFyYSBjYWRhIGNsdXN0ZXIgZ2VuZXJhZG8gY29uIGVsIG1vZGVsbyBkZSBrLW1lYW5zLgpgYGB7cn0KZ3JvdXBlZF9tdXNocm9vbSA8LSBkZiAlPiUKICBncm91cF9ieShjbHVzdGVyKSAlPiUKICBzdW1tYXJpc2UoCiAgICBtZWFuX2NhcF9kaWFtZXRlciA9IG1lYW4oY2FwLmRpYW1ldGVyKSwKICAgIG1lYW5fc3RlbV9oZWlnaHQgPSBtZWFuKHN0ZW0uaGVpZ2h0KSwKICAgIG1lYW5fc3RlbV93aWR0aCA9IG1lYW4oc3RlbS53aWR0aCkKICApCgpncm91cGVkX211c2hyb29tCmBgYAoKQSBwYXJ0aXIgZGUgZXN0ZSBtb21lbnRvLCBoZW1vcyBkZWNpZGlkbyBtb2RpZmljYXIgZWwgZGF0YXNldCBhY3R1YWwgZGViaWRvIGEgcXVlIHBhcmEgYXBsaWNhciB0w6ljbmljYXMgY29tbyAic2lsaG91ZXR0ZSIgbyAiZGVuZHJvZ3JhbSIgKHBhcmEgZWwgY2FzbyBkZSBjbHVzdGVyaW5nIGplcsOhcnF1aWNvKSBlcyBuZWNlc2FyaW8gcXVlIGVsIGRhdGFzZXQgc2VhIGRlIG1lbm9yIHRhbWHDsW8uClBhcmEgbWFudGVuZXIgbGEgcHJvcG9yY2nDs24gZGUgbG9zIGRhdG9zIGRlIGNhZGEgY2xhc2UsIHNlIHJlZHVjaXLDoSBlbCBkYXRhc2V0IGhhY2llbmRvIHVzbyBkZSBjcmVhdGVEYXRhUGFydGl0aW9uLCBtYW50ZW5pZW5kbyDDum5pY2FtZW50ZSB1biAxJSBkZSBsb3MgZGF0b3MgaW5pY2lhbGVzIHBhcmEgZWwgYW7DoWxpc2lzLgoKYGBge3J9CnNldC5zZWVkKDQyKQpzcGxpdCA8LSBjcmVhdGVEYXRhUGFydGl0aW9uKG11c2hyb29tJGNsYXNzLCBwID0gMC4wMSkKc21hbGxlcl9kZiA8LSBtdXNocm9vbVtzcGxpdCRSZXNhbXBsZTEsIF0KYGBgCgpDb21wcm9iYW1vcyBxdWUgbGEgcHJvcG9yY2nDs24gZGUgZGF0b3MgZGUgY2FkYSBjbGFzZSBzZSBtYW50aWVuZSBhbCBoYWNlciBsYSBwYXJ0aWNpw7NuLgpgYGB7cn0KaW5pdGlhbF9jbGFzc19wcm9wIDwtIHRhYmxlKG11c2hyb29tJGNsYXNzKSAvIG5yb3cobXVzaHJvb20pCnNtYWxsZXJfY2xhc3NfcHJvcCA8LSB0YWJsZShzbWFsbGVyX2RmJGNsYXNzKSAvIG5yb3coc21hbGxlcl9kZikKCnByaW50KGluaXRpYWxfY2xhc3NfcHJvcCkKcHJpbnQoc21hbGxlcl9jbGFzc19wcm9wKQpgYGAKCllhIHBvZGVtb3MgdHJhYmFqYXIgY29uIGVsIGRhdGFzZXQgcmVkdWNpZG8uIENvbXByb2JhbW9zIHF1ZSBlbCBkYXRhc2V0IGFob3JhIGN1ZW50YSBjb24gNjExIGVqZW1wbG9zIHkgMyBhdHJpYnV0b3MuCmBgYHtyfQpzbWFsbGVyX2RmIDwtIHNtYWxsZXJfZGZbLCBudW1lcmljYWxfZmVhdHVyZXNdCmRpbShzbWFsbGVyX2RmKQpgYGAKCkVuIHByaW1lciBsdWdhciwgdmlzdWFsaXphcmVtb3MgbGEgZGlzdHJpYnVjacOzbiBpbmljaWFsIGRlIGxvcyBkYXRvcyBhIHRyYXbDqXMgZGUgdW5hIGdyw6FmaWNhIDNELgoKYGBge3J9CnBsb3RfbHkoc21hbGxlcl9kZiwKICB4ID0gfmNhcC5kaWFtZXRlciwgeSA9IH5zdGVtLmhlaWdodCwKICB6ID0gfnN0ZW0ud2lkdGgKKSAlPiUKICBhZGRfbWFya2VycyhzaXplID0gMS41KQpgYGAKCkEgY29udGludWFjacOzbiwgdmFtb3MgYSBlc3R1ZGlhciBjdcOhbCBzZXLDrWEgZWwgbsO6bWVybyDDs3B0aW1vIGRlIGNsdXN0ZXJzIHBhcmEgZWwgZGF0YXNldCByZWR1Y2lkbyBoYWNpZW5kbyB1c28gZGUgbGEgbWVkaWRhIGRlIGJvbmRhZCBpbnRlcm5hICJzaWxob3VldHRlIi4gUGFyYSBlbGxvLCB1dGlsaXphcmVtb3MgbGEgZnVuY2nDs24gZnZpel9uYmNsdXN0IGRlIGZhY3RvZXh0cmEuClNpbGhvdWV0dGUgZXMgdW5hIG1lZGlkYSBxdWUgc2lydmUgcGFyYSB2YWxpZGFyIGVsIG7Dum1lcm8gZGUgY2x1c3RlcnMuIFNlIGNhbGN1bGEgY29tbyBsYSBkaWZlcmVuY2lhIGVudHJlIGxhIGRpc3RhbmNpYSBtZWRpYSBkZSB1biBwdW50byBhIGxvcyBwdW50b3MgZGUgc3UgcHJvcGlvIGNsdXN0ZXIgeSBsYSBkaXN0YW5jaWEgbWVkaWEgZGUgdW4gcHVudG8gYSBsb3MgcHVudG9zIGRlIHN1IGNsdXN0ZXIgbcOhcyBjZXJjYW5vLgoKYGBge3J9CmZ2aXpfbmJjbHVzdChzbWFsbGVyX2RmLCBGVU5jbHVzdGVyID0ga21lYW5zLCBtZXRob2QgPSAic2lsaG91ZXR0ZSIpCmBgYAoKU2Vnw7puIGxhIGdyw6FmaWNhLCBwb2RlbW9zIGFmaXJtYXIgcXVlIGVsIG7Dum1lcm8gw7NwdGltbyBkZSBjbHVzdGVycyBlcyAyLgoKUG9yIMO6bHRpbW8sIHZpc3VhbGl6YW1vcyBsYSBkaXN0cmlidWNpw7NuIGRlIGxvcyBkYXRvcyBlbiBmdW5jacOzbiBkZSBsb3MgY2x1c3RlcnMgb2J0ZW5pZG9zLgoKYGBge3J9CmttX3NtX21vZGVsIDwtIGttZWFucyhzbWFsbGVyX2RmLCBjZW50ZXIgPSAyLCBuc3RhcnQgPSAyMCkKY2x1c3RlciA8LSBmYWN0b3Ioa21fc21fbW9kZWwkY2x1c3RlcikKCnBsb3RfbHkoc21hbGxlcl9kZiwKICB4ID0gfmNhcC5kaWFtZXRlciwgeSA9IH5zdGVtLmhlaWdodCwKICB6ID0gfnN0ZW0ud2lkdGgsIGNvbG9yID0gfmNsdXN0ZXIKKSAlPiUKICBhZGRfbWFya2VycyhzaXplID0gMS41KQpgYGAKClBvciDDumx0aW1vLCBjYWxjdWxhbW9zIGVsIHZhbG9yIHByb21lZGlvIGRlIGxhcyB2YXJpYWJsZXMgcGFyYSBjYWRhIGNsdXN0ZXIgZ2VuZXJhZG8gY29uIGVsIG1vZGVsbyBkZSBrLW1lYW5zLgpgYGB7cn0KZ3JvdXBlZF9zbV9tdXNocm9vbSA8LSBzbWFsbGVyX2RmICU+JQogIG11dGF0ZShjbHVzdGVyID0gY2x1c3RlcikgJT4lCiAgZ3JvdXBfYnkoY2x1c3RlcikgJT4lCiAgc3VtbWFyaXNlKAogICAgbWVhbl9jYXBfZGlhbWV0ZXIgPSBtZWFuKGNhcC5kaWFtZXRlciksCiAgICBtZWFuX3N0ZW1faGVpZ2h0ID0gbWVhbihzdGVtLmhlaWdodCksCiAgICBtZWFuX3N0ZW1fd2lkdGggPSBtZWFuKHN0ZW0ud2lkdGgpCiAgKQpncm91cGVkX3NtX211c2hyb29tCmBgYAoKIyMjIENsdXN0ZXJpbmcgamVyw6FycXVpY288YSBuYW1lPSJoaWVyYXJjaGljYWwiPjwvYT4KCkEgY29udGludWFjacOzbiwgdmFtb3MgYSBhcGxpY2FyIGVsIGFsZ29yaXRtbyBkZSBjbHVzdGVyaW5nIGplcsOhcnF1aWNvIGEgbnVlc3RybyBkYXRhc2V0IHJlZHVjaWRvLiBQYXJhIGVsbG8sIHV0aWxpemFyZW1vcyBsYSBmdW5jacOzbiBoY2x1c3QuIFByaW1lcm8sIGNhbGN1bGFtb3MgbGEgZGlzdGFuY2lhIGVudHJlIGxvcyBwdW50b3MgZGVsIGRhdGFzZXQuCmBgYHtyfQpkaXN0YW5jZSA8LSBkaXN0KHNtYWxsZXJfZGYpCmhjX21vZGVsIDwtIGhjbHVzdChkaXN0YW5jZSkKYGBgCgpSZXByZXNlbnRhbW9zIGVsIGRlbmRyb2dyYW1hIHBhcmEgdmlzdWFsaXphciBsYSBkaXN0cmlidWNpw7NuIGRlIGxvcyBkYXRvcyBlbiBmdW5jacOzbiBkZSBsb3MgY2x1c3RlcnMgb2J0ZW5pZG9zLgoKYGBge3J9CmRlbmRfbW9kZWxvIDwtIGFzLmRlbmRyb2dyYW0oaGNfbW9kZWwpCnBsb3QoZGVuZF9tb2RlbG8pCmBgYAoKSGFzdGEgYWhvcmEsIGhlbW9zIG9idGVuaWRvIGxhIGplcmFycXXDrWEgZGUgbG9zIGRhdG9zLCBwZXJvIGxvIHF1ZSByZWFsbWVudGUgbm9zIGludGVyZXNhIGVzIGxhIGNsYXNpZmljYWNpw7NuIGRlIGxvcyBkYXRvcyBlbiBmdW5jacOzbiBkZSBsb3MgY2x1c3RlcnMuCkNvcnRhcmVtb3MgZWwgZGVuZHJvZ3JhbWEgZW4gdW4gcHVudG8gcXVlIG5vcyBpbnRlcmVzZSBwYXJhIG9idGVuZXIgbG9zIGNsdXN0ZXJzLiBFbiBlc3RlIGNhc28sIGhlbW9zIGRlY2lkaWRvIGNvcnRhciBlbCBkZW5kcm9ncmFtYSBlbiA5MCBwYXJhIG9idGVuZXIgdW5hIHZpc3VhbGl6YWNpw7NuIGRlbCBkZW5kb2dyYW1hIGNvcnRhZG8uCmBgYHtyfQpjdXQgPC0gMC45CgpkZW5kX21vZGVsbyAlPiUKICBjb2xvcl9icmFuY2hlcyhoID0gY3V0KSAlPiUKICBjb2xvcl9sYWJlbHMoaCA9IGN1dCkgJT4lCiAgcGxvdCgpCmBgYAoKUGFyYSBvYnRlbmVyIGVsIG7Dum1lcm8gw7NwdGltbyBkZSBjbHVzdGVyLCBoYXJlbW9zIHVzbyBkZSBsYSBtZWRpZGEgaW50ZXJuYSBkZSBib25kYWQgc2lsaG91ZXR0ZS4gUGFyYSBlbGxvLCB1dGlsaXphcmVtb3MgbGEgZnVuY2nDs24gZnZpel9uYmNsdXN0IGRlIGZhY3RvZXh0cmEuCgpgYGB7cn0KZnZpel9uYmNsdXN0KHNtYWxsZXJfZGYsIEZVTmNsdXN0ZXIgPSBoY3V0LCBtZXRob2QgPSAic2lsaG91ZXR0ZSIpCmBgYAoKQ29tcHJvYmFtb3MgcXVlIGVuIGVzdGUgY2FzbywgZWwgbsO6bWVybyDDs3B0aW1vIGRlIGNsdXN0ZXJzIHBvZHLDrWEgc2VyIDIgbyAzLCB5YSBxdWUgZWwgdmFsb3IgZGUgc2lsaG91ZXR0ZSBlcyBtdXkgc2ltaWxhciBwYXJhIGFtYm9zIGNhc29zLiBFbiBlc3RlIGNhc28sIGhlbW9zIGRlY2lkaWRvIHV0aWxpemFyIDIgY2x1c3RlcnMgcGFyYSBwb2RlciBjb21wYXJhciBwb3N0ZXJpb3JtZW50ZSBsb3MgcmVzdWx0YWRvcyBjb24gbG9zIG9idGVuaWRvcyBjb24gZWwgYWxnb3JpdG1vIGRlIGstbWVhbnMuCgpDYWxjdWxhbW9zIGxhIGFncnVwYWNpw7NuIGRlbCBtb2RlbG8gZW4gZnVuY2nDs24gZGVsIG7Dum1lcm8gZGUgY2x1c3RlcnMgcXVlIGhlbW9zIGRlY2lkaWRvIHV0aWxpemFyLiBBZGVtw6FzLCBjYWxjdWxhbW9zIGVsIHByb21lZGlvIGRlIGxvcyBkYXRvcyBkZSBjYWRhIGNsdXN0ZXIgcGFyYSB2ZXIgc2kgcG9kZW1vcyBzYWNhciBhbGd1bmEgY29uY2x1c2nDs24uCgpgYGB7cn0KanFfY2x1c3RlciA8LSBjdXRyZWUoaGNfbW9kZWwsIGsgPSAyKQoKZ3JvdXBlZF9tdXNocm9vbSA8LSBzbWFsbGVyX2RmICU+JQogIG11dGF0ZShjbHVzdGVyID0ganFfY2x1c3RlcikgJT4lCiAgZ3JvdXBfYnkoY2x1c3RlcikgJT4lCiAgc3VtbWFyaXNlX2FsbChtZWFuKQpncm91cGVkX211c2hyb29tCmBgYAoKVmlzdWFsaXphbW9zIGxhIGFncnVwYWNpw7NuIGRlIGxvcyBkYXRvcyBlbiBmdW5jacOzbiBkZSBsb3MgY2x1c3RlcnMgb2J0ZW5pZG9zLgoKYGBge3J9CmpxX2NsdXN0ZXIgPC0gZmFjdG9yKGpxX2NsdXN0ZXIpCgpwbG90X2x5KHNtYWxsZXJfZGYsCiAgeCA9IH5jYXAuZGlhbWV0ZXIsIHkgPSB+c3RlbS5oZWlnaHQsCiAgeiA9IH5zdGVtLndpZHRoLAogIGNvbG9yID0gfmpxX2NsdXN0ZXIKKSAlPiUKICBhZGRfbWFya2VycyhzaXplID0gMS41KQpgYGAKCkNvbiBlbCBvYmpldGl2byBkZSBjb21wYXJhciBsb3MgcmVzdWx0YWRvcyBvYnRlbmlkb3MgZW4gbG9zIGRvcyBhbGdvcml0bW9zLCB2YW1vcyBhIGNhbGN1bGFyIGVsIHJlbmRpbWllbnRvIGRlIGNhZGEgdW5vIGRlIGVsbG9zLCBoYWNpZW5kbyB1c28gZGVsIGFjY3VyYWN5IGNvbW8gbWVkaWRhIGRlIGJvbmRhZCBleHRlcm5hLgoKRW4gcHJpbWVyIGx1Z2FyLCBjYWxjdWxhbW9zIGVsIGFjY3VyYWN5IGRlbCBtb2RlbG8gZGUgay1tZWFucy4gU3Vwb25kcmVtb3MgcXVlIGxhIGNsYXNlIDEgZXMgbGEgY2xhc2UgImUiIHkgbGEgY2xhc2UgMiBlcyBsYSBjbGFzZSAicCIuClBhcmEgZWxsbywgb2J0ZW5lbW9zIGxhcyBjbGFzZXMgcmVhbGVzIHkgbGFzIGNsYXNlcyBwcmVkaWNoYXMsIHkgY2FsY3VsYW1vcyBlbCBhY2N1cmFjeS4KClZvbHZlbW9zIGEgb2J0ZW5lciBlbCBkYXRhc2V0IHJlZHVjaWRvIHBhcmEgcG9kZXIgdGVuZXIgbGFzIGNsYXNlcyByZWFsZXMuCmBgYHtyfQpzbWFsbGVyX2RmIDwtIG11c2hyb29tW3NwbGl0JFJlc2FtcGxlMSwgXQpgYGAKCmBgYHtyfQpyZWFsX2NsYXNzZXMgPC0gaWZlbHNlKHNtYWxsZXJfZGYkY2xhc3MgPT0gImUiLCAxLCAyKQpwcmVkaWN0ZWRfY2xhc3NlcyA8LSBrbV9zbV9tb2RlbCRjbHVzdGVyCnByZWRpY3RlZF9jbGFzc2VzIDwtIGFzLm51bWVyaWMocHJlZGljdGVkX2NsYXNzZXMpCmBgYAoKYGBge3J9CmFjY3VyYWN5IDwtIHN1bShyZWFsX2NsYXNzZXMgPT0gcHJlZGljdGVkX2NsYXNzZXMpIC8gbGVuZ3RoKHJlYWxfY2xhc3NlcykKcHJpbnQoYWNjdXJhY3kpCmBgYAoKSGFjZW1vcyBsbyBtaXNtbyBjb24gZWwgbW9kZWxvIGRlIGNsdXN0ZXJpbmcgamVyw6FycXVpY28sIHBlcm8gZW4gZXN0ZSBjYXNvLCBzdXBvbmRyZW1vcyBxdWUgbGEgY2xhc2UgMSBlcyBsYSBjbGFzZSAicCIgeSBsYSBjbGFzZSAyIGVzIGxhIGNsYXNlICJlIi4KYGBge3J9CnJlYWxfY2xhc3NlcyA8LSBpZmVsc2Uoc21hbGxlcl9kZiRjbGFzcyA9PSAiZSIsIDIsIDEpCnByZWRpY3RlZF9jbGFzc2VzIDwtIGFzLm51bWVyaWMoanFfY2x1c3RlcikKYGBgCgpgYGB7cn0KYWNjdXJhY3kgPC0gc3VtKHJlYWxfY2xhc3NlcyA9PSBwcmVkaWN0ZWRfY2xhc3NlcykgLyBsZW5ndGgocmVhbF9jbGFzc2VzKQpwcmludChhY2N1cmFjeSkKYGBgCgpUcmFzIGNvbXBhcmFyIGxvcyByZXN1bHRhZG9zIG9idGVuaWRvcyBlbiBsb3MgZG9zIGFsZ29yaXRtb3MsIHBvZGVtb3MgYWZpcm1hciBxdWUgZWwgbW9kZWxvIGRlIGNsdXN0ZXJpbmcgamVyw6FycXVpY28gaGEgb2J0ZW5pZG8gdW4gYWNjdXJhY3kgbWF5b3IgcGFyYSBlc3RlIGRhdGFzZXQu